Merge pull request #91 from Qortal/feature/framework-implementation

Feature/framework implementation
This commit is contained in:
Phillip
2025-08-02 17:54:26 +03:00
committed by GitHub
191 changed files with 15234 additions and 11606 deletions

View File

@@ -1,18 +0,0 @@
module.exports = {
env: { browser: true, amd: true, node: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
};

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
build
dist

View File

@@ -1,10 +1,23 @@
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"endOfLine": "lf",
"experimentalTernaries": false,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": true,
"tabWidth": 2,
"semi": true
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

37
eslint.config.js Normal file
View File

@@ -0,0 +1,37 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import prettierPlugin from 'eslint-plugin-prettier';
import prettierConfig from 'eslint-config-prettier';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
prettier: prettierPlugin,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'prettier/prettier': 'error',
},
},
{
// This disables ESLint rules that would conflict with Prettier
name: 'prettier-config',
rules: prettierConfig.rules,
}
);

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -8,6 +8,8 @@
</head>
<body>
<div id="root"></div>
<div id="dropdown-portal-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3197
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
@@ -15,31 +15,32 @@
"@mui/icons-material": "^7.1.0",
"@mui/lab": "^7.0.0-beta.12",
"@mui/material": "^7.1.0",
"@preact/signals-react": "^2.3.0",
"@reduxjs/toolkit": "^2.5.0",
"compressorjs": "^1.2.1",
"dompurify": "^3.2.3",
"framer-motion": "^12.23.6",
"i18n": "^0.15.1",
"idb-keyval": "^6.2.2",
"jotai": "^2.12.4",
"localforage": "^1.10.0",
"mediainfo.js": "^0.3.5",
"moment": "^2.30.1",
"qapp-core": "^1.0.29",
"qapp-core": "^1.0.52",
"quill": "^2.0.2",
"quill-image-resize-module-react": "^3.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.5",
"react-i18next": "^15.5.3",
"react-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.14.0",
"react-quill-new": "^3.3.3",
"react-redux": "^9.2.0",
"react-rnd": "^10.4.14",
"react-router-dom": "^7.1.1",
"react-router-dom": "^7.6.2",
"react-toastify": "^11.0.2",
"redux-persist": "^6.0.0",
"short-unique-id": "^5.2.0",
"ts-key-enum": "^2.0.13"
"short-unique-id": "^5.2.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.2",
@@ -48,8 +49,10 @@
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.15.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2",
"vite": "^6.3.5"
"vite": "^6.3.5",
"vite-plugin-static-copy": "^3.0.0"
}
}

BIN
public/MediaInfoModule.wasm Normal file

Binary file not shown.

View File

@@ -1,42 +1,23 @@
import { SubscriptionData } from "./components/common/ContentButtons/SubscribeButton.tsx";
import { store } from "./state/store.ts";
import { SubscriptionData } from './components/common/ContentButtons/SubscribeButton.tsx';
export const getUserName = async () => {
const account = await qortalRequest({
action: "GET_USER_ACCOUNT",
action: 'GET_USER_ACCOUNT',
});
const nameData = await qortalRequest({
action: "GET_ACCOUNT_NAMES",
action: 'GET_ACCOUNT_NAMES',
address: account.address,
});
if (nameData?.length > 0) return nameData[0].name;
else return "";
else return '';
};
export const filterVideosByName = (
subscriptionList: SubscriptionData[],
userName: string
) => {
return subscriptionList.filter(item => {
return subscriptionList.filter((item) => {
return item.userName === userName;
});
};
export const subscriptionListFilter = async (reset = true) => {
const filteredSubscriptionList =
store.getState().video.filteredSubscriptionList;
const isFilteredSubscriptionListEmpty = filteredSubscriptionList.length === 0;
if (!reset && !isFilteredSubscriptionListEmpty) {
return filteredSubscriptionList;
}
const subscriptionList = store.getState().persist.subscriptionList;
const filterByUserName =
store.getState().persist.subscriptionListFilter === "currentNameOnly";
const userName = await getUserName();
if (filterByUserName && userName) {
return filterVideosByName(subscriptionList, userName);
} else return subscriptionList;
};

View File

@@ -1,66 +1,14 @@
import { CssBaseline } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { Provider, useSelector } from "react-redux";
import { Route, Routes } from "react-router-dom";
import { PersistGate } from "redux-persist/integration/react";
import { subscriptionListFilter } from "./App-Functions.ts";
import Notification from "./components/common/Notification/Notification";
import { useIframe } from "./hooks/useIframe.tsx";
import { IndividualProfile } from "./pages/ContentPages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/ContentPages/PlaylistContent/PlaylistContent";
import { VideoContent } from "./pages/ContentPages/VideoContent/VideoContent";
import { Home } from "./pages/Home/Home";
import { setFilteredSubscriptions } from "./state/features/videoSlice.ts";
import { store, persistor, RootState } from "./state/store";
import { darkTheme, lightTheme } from "./styles/theme";
import DownloadWrapper from "./wrappers/DownloadWrapper";
import GlobalWrapper from "./wrappers/GlobalWrapper";
import { ScrollWrapper } from "./wrappers/ScrollWrapper.tsx";
import { QappCoreWrapper } from "./QappCoreWrapper.tsx";
import Notification from './components/common/Notification/Notification';
import { Routes } from './Routes.tsx';
import ThemeProviderWrapper from './styles/theme-provider.tsx';
function App() {
// const themeColor = window._qdnTheme
const [theme, setTheme] = useState("dark");
useIframe();
useEffect(() => {
subscriptionListFilter(false).then(filteredList => {
store.dispatch(setFilteredSubscriptions(filteredList));
});
}, []);
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
<QappCoreWrapper>
<Notification />
<DownloadWrapper>
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
<ScrollWrapper>
<CssBaseline />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/video/:name/:id" element={<VideoContent />} />
<Route
path="/playlist/:name/:id"
element={<PlaylistContent />}
/>
<Route
path="/channel/:name"
element={<IndividualProfile />}
/>
</Routes>
</ScrollWrapper>
</GlobalWrapper>
</DownloadWrapper>
</QappCoreWrapper>
</ThemeProvider>
</PersistGate>
</Provider>
<ThemeProviderWrapper>
<Notification />
<Routes />
</ThemeProviderWrapper>
);
}

28
src/AppWrapper.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { GlobalProvider } from 'qapp-core';
import Layout from './Layout';
import { ScrollWrapper } from './wrappers/ScrollWrapper';
import GlobalWrapper from './wrappers/GlobalWrapper';
export const AppWrapper = () => {
return (
<GlobalProvider
config={{
auth: {
authenticateOnMount: true,
balanceSetting: {
interval: 120000,
},
},
publicSalt: 'usVbeM9YpjGCbLrTcc78YJS0ap1AxDkHAOMZrp3+wDY=',
appName: 'Q-Tube',
enableGlobalVideoFeature: true,
}}
>
<GlobalWrapper>
<ScrollWrapper>
<Layout />
</ScrollWrapper>
</GlobalWrapper>
</GlobalProvider>
);
};

136
src/Layout.tsx Normal file
View File

@@ -0,0 +1,136 @@
import { Outlet } from 'react-router-dom';
import { useIframe } from './hooks/useIframe';
import { Box, useTheme } from '@mui/material';
import { useAtom, useSetAtom } from 'jotai';
import { isSideBarExpandedAtom, scrollRefAtom } from './state/global/navbar';
import NavBar from './components/layout/Navbar/Navbar';
import { Sidenav } from './components/layout/Sidenav/Sidenav';
import { namesAtom } from './state/global/names';
import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { PageTransition } from './components/common/PageTransition';
import { useIsSmall } from './hooks/useIsSmall';
import { useIsMobile } from './hooks/useIsMobile';
const Layout = () => {
const isSmall = useIsSmall();
const isMobile = useIsMobile();
const scrollRef = useRef<any>(null);
const setScrollRef = useSetAtom(scrollRefAtom);
const theme = useTheme();
useEffect(() => {
setScrollRef(scrollRef);
}, [setScrollRef]);
const [names] = useAtom(namesAtom);
const [showNavbar, setShowNavbar] = useState(true);
const lastScrollY = useRef(0);
useIframe();
useEffect(() => {
const scrollElement = scrollRef.current;
if (!scrollElement) return;
let lastY = 0;
let ticking = false;
const handleScroll = () => {
const currentY = scrollElement.scrollTop;
const deltaY = currentY - lastY;
if (Math.abs(deltaY) < 70) {
ticking = false;
return; // skip tiny scrolls
}
if (deltaY > 0 && currentY > 50) {
setShowNavbar(false); // scroll down
} else {
setShowNavbar(true); // scroll up
}
lastY = currentY;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
requestAnimationFrame(handleScroll);
ticking = true;
}
};
scrollElement.addEventListener('scroll', onScroll);
return () => scrollElement.removeEventListener('scroll', onScroll);
}, []);
return (
<Box display="flex" flexDirection="column" height="100vh" overflow="hidden">
<AnimatePresence>
<motion.div
animate={
showNavbar
? { height: '64px', opacity: 1, y: 0 }
: { height: '0px', opacity: 0, y: -20 }
}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{
overflow: 'hidden',
zIndex: 1000,
}}
>
<NavBar allNames={names} />
</motion.div>
</AnimatePresence>
<Box
sx={{
width: '100%',
display: 'flex',
height: '100%',
overflow: 'hidden',
}}
>
<Sidenav allNames={names} />
{/* Main Content */}
<Box
ref={scrollRef}
id="main-box"
component="main"
flex={1}
p={isMobile ? 0 : 2}
sx={{
overflowY: 'scroll',
'::-webkit-scrollbar-track': {
backgroundColor: 'transparent',
},
'::-webkit-scrollbar': {
width: isMobile ? '0px' : '16px',
height: '10px',
display: isMobile ? 'none' : 'initial',
},
'::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(63, 67, 80, 0.24)',
borderRadius: '8px',
backgroundClip: 'content-box',
border: '4px solid transparent',
transition: '0.3s background-color',
},
'::-webkit-scrollbar-thumb:hover': {
backgroundColor: 'rgba(63, 67, 80, 0.50)',
},
}}
>
<Outlet />
</Box>
</Box>
</Box>
);
};
export default Layout;

View File

@@ -1,24 +0,0 @@
import { GlobalProvider } from 'qapp-core'
import { useSelector } from 'react-redux';
import { RootState } from './state/store';
export const QappCoreWrapper = ({children}) => {
const { user } = useSelector((state: RootState) => state.auth);
return (
<GlobalProvider
config={{
auth: {
authenticateOnMount: false,
userAccountInfo: {
address: user?.address,
publicKey: user?.publicKey
}
},
publicSalt: "usVbeM9YpjGCbLrTcc78YJS0ap1AxDkHAOMZrp3+wDY=",
appName: "Q-Tube",
}}
>
{children}
</GlobalProvider>
)
}

79
src/Routes.tsx Normal file
View File

@@ -0,0 +1,79 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import App from './App';
import { AppWrapper } from './AppWrapper';
import { Home } from './pages/Home/Home';
import { VideoContent } from './pages/ContentPages/VideoContent/VideoContent';
import { PlaylistContent } from './pages/ContentPages/PlaylistContent/PlaylistContent';
import { IndividualProfile } from './pages/ContentPages/IndividualProfile/IndividualProfile';
import { Search } from './pages/Search/Search';
import { Subscriptions } from './pages/Subscriptions/Subscriptions';
import { Bookmarks } from './pages/Bookmarks/Bookmarks';
import { History } from './pages/History/History';
interface CustomWindow extends Window {
_qdnBase: string;
}
const customWindow = window as unknown as CustomWindow;
const baseUrl = customWindow?._qdnBase || '';
export function Routes() {
const router = createBrowserRouter(
[
{
path: '/',
element: <AppWrapper />, // GlobalProvider wrapper
children: [
{
index: true,
element: <Home />,
},
{
path: 'subscriptions',
element: <Subscriptions />,
},
{
path: 'bookmarks',
element: <Bookmarks />,
},
{
path: 'history',
element: <History />,
},
{
path: 'search',
element: <Search />,
},
{
path: 'video/:name/:id',
element: <VideoContent />,
},
{
path: 'playlist/:name/:id',
element: <PlaylistContent />,
},
{
path: 'playlist/:name/:id/:s/:n/:i',
element: <PlaylistContent />,
},
{
path: 'channel/:name',
element: <IndividualProfile />,
},
{
path: 'channel/:name/:section',
element: <IndividualProfile />,
},
{
path: 'channel/:name/:section',
element: <IndividualProfile />,
},
],
},
],
{
basename: baseUrl,
}
);
return <RouterProvider router={router} />;
}

View File

@@ -5,9 +5,11 @@ import {
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import React from "react";
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
} from '@mui/material';
import React from 'react';
import { CardContentContainerComment } from '../common/Comments/Comments-styles';
import { useIsSmall } from '../../hooks/useIsSmall';
import { AddToBookmarks } from '../common/ContentButtons/AddToBookmarks';
interface PlaylistsProps {
playlistData;
@@ -21,25 +23,34 @@ export const Playlists = ({
onClick,
sx,
}: PlaylistsProps) => {
const isSmall = useIsSmall();
const theme = useTheme();
const isScreenSmall = !useMediaQuery(`(min-width:700px)`);
const PlaylistsHeight = "36vw"; // This is videoplayer width * 9/16 (inverse of aspect ratio)
return (
<Box
sx={{
width: "100%",
height: isScreenSmall ? "200px" : PlaylistsHeight,
// width: "100%",
...sx,
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
}}
>
<AddToBookmarks
metadataReference={{
identifier: playlistData?.identifier,
service: 'PLAYLIST',
name: playlistData?.name,
}}
type="playlist"
/>
<CardContentContainerComment
sx={{
marginTop: "0px",
height: "100%",
overflow: "auto",
marginTop: '0px',
height: '100%',
gap: '3px',
padding: '3px',
}}
>
{playlistData?.videos?.map((vid, index) => {
@@ -50,15 +61,20 @@ export const Playlists = ({
<Box
key={vid?.identifier}
sx={{
display: "flex",
gap: "10px",
width: "100%",
background: isCurrentVidPlaying && theme.palette.primary.main,
alignItems: "center",
padding: "10px",
borderRadius: "5px",
cursor: isCurrentVidPlaying ? "default" : "pointer",
userSelect: "none",
display: 'flex',
gap: '10px',
width: '100%',
alignItems: 'center',
padding: '10px',
borderRadius: '5px',
cursor: isCurrentVidPlaying ? 'default' : 'pointer',
userSelect: 'none',
border: '1px solid rgba(255, 255, 255, 0.23)',
...(isCurrentVidPlaying && {
background: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}),
}}
onClick={() => {
if (isCurrentVidPlaying) return;
@@ -68,16 +84,16 @@ export const Playlists = ({
>
<Typography
sx={{
fontSize: "18px",
fontWeight: "bold",
fontSize: isSmall ? '16px' : '18px',
fontWeight: 'bold',
}}
>
{index + 1}
</Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
fontSize: isSmall ? '16px' : '18px',
wordBreak: 'break-word',
}}
>
{vid?.metadata?.title}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from 'react';
import {
AddCoverImageButton,
AddLogoIcon,
@@ -6,13 +6,11 @@ import {
CrowdfundActionButton,
CrowdfundActionButtonRow,
CustomInputField,
CustomSelect,
LogoPreviewRow,
ModalBody,
LogoPreviewRow,
NewCrowdfundTitle,
StyledButton,
TimesIcon,
} from "./Upload-styles.tsx";
} from './Upload-styles.tsx';
import {
Box,
FormControl,
@@ -24,71 +22,46 @@ import {
SelectChangeEvent,
Typography,
useTheme,
} from "@mui/material";
import ShortUniqueId from "short-unique-id";
import { useDispatch, useSelector } from "react-redux";
import AddBoxIcon from "@mui/icons-material/AddBox";
import { useDropzone } from "react-dropzone";
} from '@mui/material';
import ShortUniqueId from 'short-unique-id';
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import {
objectToBase64,
objectToFile,
uint8ArrayToBase64,
} from "../../../utils/PublishFormatter.ts";
import { RootState } from "../../../state/store.ts";
import {
upsertVideosBeginning,
addToHashMap,
upsertVideos,
setEditVideo,
updateVideo,
updateInHashMap,
setEditPlaylist,
} from "../../../state/features/videoSlice.ts";
import ImageUploader from "../../common/ImageUploader.tsx";
import { categories, subCategories } from "../../../constants/Categories.ts";
import { Playlists } from "../../Playlists/Playlists.tsx";
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit.tsx";
import { TextEditor } from "../../common/TextEditor/TextEditor.tsx";
import { extractTextFromHTML } from "../../common/TextEditor/utils.ts";
import { objectToBase64 } from '../../../utils/PublishFormatter.ts';
import ImageUploader from '../../common/ImageUploader.tsx';
import { categories, subCategories } from '../../../constants/Categories.ts';
import { Playlists } from '../../Playlists/Playlists.tsx';
import { PlaylistListEdit } from '../PlaylistListEdit/PlaylistListEdit.tsx';
import { TextEditor } from '../../common/TextEditor/TextEditor.tsx';
import { extractTextFromHTML } from '../../common/TextEditor/utils.ts';
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../../../constants/Identifiers.ts";
} from '../../../constants/Identifiers.ts';
import { useAuth, useGlobal } from 'qapp-core';
import {
AltertObject,
setNotificationAtom,
} from '../../../state/global/notifications.ts';
import { useAtom, useSetAtom } from 'jotai';
import { editPlaylistAtom } from '../../../state/publish/playlist.ts';
import { useTranslation } from 'react-i18next';
const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 });
interface NewCrowdfundProps {
editId?: string;
editContent?: null | {
title: string;
user: string;
coverImage: string | null;
};
}
interface VideoFile {
file: File;
title: string;
description: string;
coverImage?: string;
}
export const EditPlaylist = () => {
const { t } = useTranslation(['core', 'category']);
const theme = useTheme();
const dispatch = useDispatch();
const username = useSelector((state: RootState) => state.auth?.user?.name);
const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address
);
const editVideoProperties = useSelector(
(state: RootState) => state.video.editPlaylistProperties
);
const { name: username, address: userAddress } = useAuth();
const setNotification = useSetAtom(setNotificationAtom);
const { lists } = useGlobal();
const [editVideoProperties] = useAtom(editPlaylistAtom);
const setEditPlaylist = useSetAtom(editPlaylistAtom);
const [playlistData, setPlaylistData] = useState<any>(null);
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [coverImage, setCoverImage] = useState<string>("");
const [title, setTitle] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [coverImage, setCoverImage] = useState<string>('');
const [videos, setVideos] = useState([]);
const [selectedCategoryVideos, setSelectedCategoryVideos] =
useState<any>(null);
@@ -96,7 +69,7 @@ export const EditPlaylist = () => {
useState<any>(null);
const isNew = useMemo(() => {
return editVideoProperties?.mode === "new";
return editVideoProperties?.mode === 'new';
}, [editVideoProperties]);
useEffect(() => {
@@ -107,83 +80,39 @@ export const EditPlaylist = () => {
}
}, [isNew]);
// useEffect(() => {
// if (editVideoProperties) {
// const descriptionString = editVideoProperties?.description || "";
// // Splitting the string at the asterisks
// const parts = descriptionString.split("**");
// // The part within the asterisks
// const extractedString = parts[1];
// // The part after the last asterisks
// const description = parts[2] || ""; // Using '|| '' to handle cases where there is no text after the last **
// setTitle(editVideoProperties?.title || "");
// setDescription(editVideoProperties?.fullDescription || "");
// setCoverImage(editVideoProperties?.videoImage || "");
// // Split the extracted string into key-value pairs
// const keyValuePairs = extractedString.split(";");
// // Initialize variables to hold the category and subcategory values
// let category, subcategory;
// // Loop through each key-value pair
// keyValuePairs.forEach((pair) => {
// const [key, value] = pair.split(":");
// // Check the key and assign the value to the appropriate variable
// if (key === "category") {
// category = value;
// } else if (key === "subcategory") {
// subcategory = value;
// }
// });
// if(category){
// const selectedOption = categories.find((option) => option.id === +category);
// setSelectedCategoryVideos(selectedOption || null);
// }
// if(subcategory){
// const selectedOption = categories.find((option) => option.id === +subcategory);
// setSelectedCategoryVideos(selectedOption || null);
// }
// }
// }, [editVideoProperties]);
const checkforPlaylist = React.useCallback(async videoList => {
const checkforPlaylist = React.useCallback(async (videoList) => {
try {
const combinedData: any = {};
const videos = [];
const videos: any[] = [];
if (videoList) {
for (const vid of videoList) {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseDataSearchVid = await response.json();
if (responseDataSearchVid?.length > 0) {
const resourceData2 = responseDataSearchVid[0];
videos.push(resourceData2);
if (resourceData2) {
videos.push(resourceData2);
}
}
}
}
combinedData.videos = videos;
setPlaylistData(combinedData);
} catch (error) {
console.log(error);
console.error(error);
}
}, []);
useEffect(() => {
if (editVideoProperties) {
setTitle(editVideoProperties?.title || "");
setTitle(editVideoProperties?.title || '');
if (editVideoProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription);
@@ -191,12 +120,12 @@ export const EditPlaylist = () => {
const paragraph = `<p>${editVideoProperties?.description}</p>`;
setDescription(paragraph);
}
setCoverImage(editVideoProperties?.image || "");
setCoverImage(editVideoProperties?.image || '');
setVideos(editVideoProperties?.videos || []);
if (editVideoProperties?.category) {
const selectedOption = categories.find(
option => option.id === +editVideoProperties.category
(option) => option.id === +editVideoProperties.category
);
setSelectedCategoryVideos(selectedOption || null);
}
@@ -208,7 +137,7 @@ export const EditPlaylist = () => {
) {
const selectedOption = subCategories[
+editVideoProperties?.category
]?.find(option => option.id === +editVideoProperties.subcategory);
]?.find((option) => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null);
}
@@ -219,69 +148,68 @@ export const EditPlaylist = () => {
}, [editVideoProperties]);
const onClose = () => {
setTitle("");
setDescription("");
setTitle('');
setDescription('');
setVideos([]);
setPlaylistData(null);
setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null);
setCoverImage("");
dispatch(setEditPlaylist(null));
setCoverImage('');
setEditPlaylist(null);
};
async function publishQDNResource() {
try {
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!coverImage) throw new Error("Please select cover image");
if (!selectedCategoryVideos) throw new Error("Please select a category");
if (!title) throw new Error('Please enter a title');
if (!description) throw new Error('Please enter a description');
if (!coverImage) throw new Error('Please select cover image');
if (!selectedCategoryVideos) throw new Error('Please select a category');
if (!editVideoProperties) return;
if (!userAddress) throw new Error("Unable to locate user address");
let errorMsg = "";
let name = "";
if (!userAddress) throw new Error('Unable to locate user address');
let errorMsg = '';
let name = '';
if (username) {
name = username;
}
if (!name) {
errorMsg =
"Cannot publish without access to your name. Please authenticate.";
'Cannot publish without access to your name. Please authenticate.';
}
if (!isNew && editVideoProperties?.user !== username) {
if (!isNew && editVideoProperties?.name !== username) {
errorMsg = "Cannot publish another user's resource";
}
if (errorMsg) {
dispatch(
setNotification({
msg: errorMsg,
alertType: "error",
})
);
const notificationObj: AltertObject = {
msg: errorMsg,
alertType: 'error',
};
setNotification(notificationObj);
return;
}
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
const subcategory = selectedSubCategoryVideos?.id || '';
const videoStructured = playlistData.videos.map(item => {
const videoStructured = playlistData.videos.map((item) => {
const descriptionVid = item?.metadata?.description;
if (!descriptionVid) throw new Error("cannot find video code");
if (!descriptionVid) throw new Error('cannot find video code');
// Split the string by ';'
const parts = descriptionVid.split(";");
const parts = descriptionVid.split(';');
// Initialize a variable to hold the code value
let codeValue = "";
let codeValue = '';
// Loop through the parts to find the one that starts with 'code:'
for (const part of parts) {
if (part.startsWith("code:")) {
codeValue = part.split(":")[1];
if (part.startsWith('code:')) {
codeValue = part.split(':')[1];
break;
}
}
if (!codeValue) throw new Error("cannot find video code");
if (!codeValue) throw new Error('cannot find video code');
return {
identifier: item.identifier,
@@ -292,8 +220,7 @@ export const EditPlaylist = () => {
});
const id = uid.rnd();
let commentsId = editVideoProperties?.id;
let commentsId = editVideoProperties?.identifier;
if (isNew) {
commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}`;
}
@@ -312,20 +239,20 @@ export const EditPlaylist = () => {
};
const codes = videoStructured
.map(item => `c:${item.code};`)
.map((item) => `c:${item.code};`)
.slice(0, 10)
.join("");
.join('');
const metadescription =
`**category:${category};subcategory:${subcategory};${codes}**` +
stringDescription.slice(0, 120);
// Description is obtained from raw data
let identifier = editVideoProperties?.id;
let identifier = editVideoProperties?.identifier;
const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/[^a-zA-Z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim()
.toLowerCase();
if (isNew) {
@@ -335,9 +262,9 @@ export const EditPlaylist = () => {
)}_${id}`;
}
const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE",
action: 'PUBLISH_QDN_RESOURCE',
name: username,
service: "PLAYLIST",
service: 'PLAYLIST',
data64: await objectToBase64(playlistObject),
title: title.slice(0, 50),
description: metadescription,
@@ -351,57 +278,47 @@ export const EditPlaylist = () => {
title: title.slice(0, 50),
description: metadescription,
id: identifier,
service: "PLAYLIST",
service: 'PLAYLIST',
user: username,
...playlistObject,
};
dispatch(updateVideo(objectToStore));
dispatch(updateInHashMap(objectToStore));
} else {
dispatch(
updateVideo({
...editVideoProperties,
...playlistObject,
})
);
dispatch(
updateInHashMap({
...editVideoProperties,
...playlistObject,
})
);
lists.updateNewResources([
{
data: playlistObject,
qortalMetadata: {
identifier: identifier,
service: 'PLAYLIST',
name: username || '',
size: 100,
updated: Date.now(),
metadata: {
title: title.slice(0, 50),
description: metadescription,
tags: [QTUBE_VIDEO_BASE],
},
created: editVideoProperties?.created,
},
},
]);
}
dispatch(
setNotification({
msg: "Playlist published",
alertType: "success",
})
);
const notificationObj: AltertObject = {
msg: 'Playlist published',
alertType: 'success',
};
setNotification(notificationObj);
onClose();
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {
notificationObj = {
msg: error || "Failed to publish update",
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || "Failed to publish update",
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || "Failed to publish update",
alertType: "error",
};
}
if (!notificationObj) return;
dispatch(setNotification(notificationObj));
const isError = error instanceof Error;
const message = isError ? error?.message : 'Failed to publish update';
const notificationObj: AltertObject = {
msg: message,
alertType: 'error',
};
setNotification(notificationObj);
throw new Error("Failed to publish update");
throw new Error('Failed to publish update');
}
}
@@ -409,7 +326,7 @@ export const EditPlaylist = () => {
event: SelectChangeEvent<string>
) => {
const optionId = event.target.value;
const selectedOption = categories.find(option => option.id === +optionId);
const selectedOption = categories.find((option) => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos = (
@@ -418,28 +335,28 @@ export const EditPlaylist = () => {
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
option => option.id === +optionId
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos(selectedOption || null);
};
const removeVideo = index => {
const removeVideo = (index) => {
const copyData = structuredClone(playlistData);
copyData.videos.splice(index, 1);
setPlaylistData(copyData);
};
const addVideo = data => {
const addVideo = (data) => {
const copyData = structuredClone(playlistData);
copyData.videos = [...copyData.videos, { ...data }];
setPlaylistData(copyData);
};
const updateVideoList = list => {
const updateVideoList = (list) => {
const copyData = structuredClone(playlistData);
copyData.videos = [...list];
setPlaylistData(copyData);
}
};
return (
<>
@@ -448,39 +365,61 @@ export const EditPlaylist = () => {
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody>
<ModalBody
sx={{
width: '100%',
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
{isNew ? (
<NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle>
<NewCrowdfundTitle>
{t('core:publish.create_new_playlist', {
postProcess: 'capitalizeFirstChar',
})}
</NewCrowdfundTitle>
) : (
<NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle>
<NewCrowdfundTitle>
{t('core:publish.update_playlist_properties', {
postProcess: 'capitalizeFirstChar',
})}
</NewCrowdfundTitle>
)}
</Box>
<>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
display: 'flex',
gap: '20px',
alignItems: 'center',
}}
>
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">Select a Category</InputLabel>
<InputLabel id="Category">
{t('core:publish.select_category', {
postProcess: 'capitalizeFirstChar',
})}
</InputLabel>
<Select
labelId="Category"
input={<OutlinedInput label="Select a Category" />}
value={selectedCategoryVideos?.id || ""}
input={
<OutlinedInput
label={t('core:publish.select_category', {
postProcess: 'capitalizeFirstChar',
})}
/>
}
value={selectedCategoryVideos?.id || ''}
onChange={handleOptionCategoryChangeVideos}
>
{categories.map(option => (
{categories.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
{t(`category:categories.${option.id}`)}
</MenuItem>
))}
</Select>
@@ -488,23 +427,35 @@ export const EditPlaylist = () => {
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">Select a Sub-Category</InputLabel>
<InputLabel id="Category">
{t('core:publish.select_subcategory', {
postProcess: 'capitalizeFirstChar',
})}
</InputLabel>
<Select
labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={e =>
input={
<OutlinedInput
label={t('core:publish.select_subcategory', {
postProcess: 'capitalizeFirstChar',
})}
/>
}
value={selectedSubCategoryVideos?.id || ''}
onChange={(e) =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
>
{subCategories[selectedCategoryVideos.id].map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
{subCategories[selectedCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{t(`category:subcategories.${option.id}`)}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
@@ -513,11 +464,13 @@ export const EditPlaylist = () => {
{!coverImage ? (
<ImageUploader onPick={(img: string) => setCoverImage(img)}>
<AddCoverImageButton variant="contained">
Add Cover Image
{t('core:publish.add_cover_image', {
postProcess: 'capitalizeFirstChar',
})}
<AddLogoIcon
sx={{
height: "25px",
width: "auto",
height: '25px',
width: 'auto',
}}
></AddLogoIcon>
</AddCoverImageButton>
@@ -527,22 +480,24 @@ export const EditPlaylist = () => {
<CoverImagePreview src={coverImage} alt="logo" />
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() => setCoverImage("")}
height={"32"}
width={"32"}
onClickFunc={() => setCoverImage('')}
height={'32'}
width={'32'}
></TimesIcon>
</LogoPreviewRow>
)}
<CustomInputField
name="title"
label="Title of playlist"
label={t('core:publish.title_playlist', {
postProcess: 'capitalizeFirstChar',
})}
variant="filled"
value={title}
onChange={e => {
onChange={(e) => {
const value = e.target.value;
const formattedValue = value.replace(
/[^a-zA-Z0-9\s-_!?]/g,
""
''
);
setTitle(formattedValue);
}}
@@ -562,14 +517,16 @@ export const EditPlaylist = () => {
/> */}
<Typography
sx={{
fontSize: "18px",
fontSize: '18px',
}}
>
Description of playlist
{t('core:publish.description_playlist', {
postProcess: 'capitalizeFirstChar',
})}
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setInlineContent={(value) => {
setDescription(value);
}}
/>
@@ -591,13 +548,15 @@ export const EditPlaylist = () => {
variant="contained"
color="error"
>
Cancel
{t('core:action.cancel', {
postProcess: 'capitalizeFirstChar',
})}
</CrowdfundActionButton>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
display: 'flex',
gap: '20px',
alignItems: 'center',
}}
>
<CrowdfundActionButton
@@ -606,7 +565,9 @@ export const EditPlaylist = () => {
publishQDNResource();
}}
>
Publish
{t('core:publish.publish_action', {
postProcess: 'capitalizeFirstChar',
})}
</CrowdfundActionButton>
</Box>
</CrowdfundActionButtonRow>

View File

@@ -1,4 +1,4 @@
import { styled } from "@mui/system";
import { styled } from '@mui/system';
import {
Accordion,
AccordionDetails,
@@ -10,9 +10,9 @@ import {
TextField,
Typography,
Select,
} from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx";
} from '@mui/material';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { TimesSVG } from '../../../assets/svgs/TimesSVG.tsx';
export const DoubleLine = styled(Typography)`
display: -webkit-box;
@@ -22,181 +22,172 @@ export const DoubleLine = styled(Typography)`
`;
export const MainContainer = styled(Grid)({
width: "100%",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
width: '100%',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
margin: 0,
});
export const MainCol = styled(Grid)(({ theme }) => ({
display: "flex",
flexDirection: "column",
alignItems: "center",
width: "100%",
padding: "20px",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
padding: '20px',
}));
export const CreateContainer = styled(Box)(({ theme }) => ({
position: "fixed",
bottom: "20px",
right: "20px",
cursor: "pointer",
position: 'fixed',
bottom: '20px',
right: '20px',
cursor: 'pointer',
background: theme.palette.background.default,
width: "50px",
height: "50px",
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "50%",
width: '50px',
height: '50px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '50%',
}));
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
position: 'absolute',
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
borderRadius: '4px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '75%',
maxWidth: '900px',
padding: '15px 35px',
display: 'flex',
flexDirection: 'column',
gap: '17px',
overflowY: 'auto',
maxHeight: '95vh',
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
theme.palette.mode === 'dark'
? '0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)'
: 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px',
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
'&::-webkit-scrollbar-track:hover': {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
'&::-webkit-scrollbar': {
width: '16px',
height: '10px',
backgroundColor: theme.palette.mode === 'light' ? '#f6f8fa' : '#292d3e',
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.mode === 'light' ? '#d3d9e1' : '#575757',
borderRadius: '8px',
backgroundClip: 'content-box',
border: '4px solid transparent',
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme.palette.mode === 'light' ? '#b7bcc4' : '#474646',
},
}));
export const NewCrowdfundTitle = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "25px",
userSelect: "none",
fontSize: '25px',
userSelect: 'none',
}));
export const NewCrowdFundFont = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "18px",
userSelect: "none",
fontSize: '18px',
userSelect: 'none',
}));
export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "18px",
userSelect: "none",
fontStyle: "italic",
textDecoration: "underline",
fontSize: '18px',
userSelect: 'none',
fontStyle: 'italic',
textDecoration: 'underline',
}));
export const CustomInputField = styled(TextField)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
borderColor: theme.palette.background.paper,
"& label": {
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& label': {
color: theme.palette.mode === 'light' ? '#808183' : '#edeef0',
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
},
"& label.Mui-focused": {
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
'& label.Mui-focused': {
color: theme.palette.mode === 'light' ? '#A0AAB4' : '#d7d8da',
},
"& .MuiInput-underline:after": {
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
'& .MuiInput-underline:after': {
borderBottomColor: theme.palette.mode === 'light' ? '#B2BAC2' : '#c9cccf',
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#E0E3E7',
},
"&:hover fieldset": {
borderColor: "#B2BAC2",
'&:hover fieldset': {
borderColor: '#B2BAC2',
},
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
'&.Mui-focused fieldset': {
borderColor: '#6F7E8C',
},
},
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiInputBase-root': {
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
},
"& [class$='-MuiFilledInput-root']": {
padding: "30px 12px 8px",
padding: '30px 12px 8px',
},
"& .MuiFilledInput-root:after": {
'& .MuiFilledInput-root:after': {
borderBottomColor: theme.palette.secondary.main,
},
}));
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
letterSpacing: '1px',
fontWeight: 400,
fontSize: "20px",
fontSize: '20px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
}));
export const CrowdfundSubTitleRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
flexDirection: "row",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
flexDirection: 'row',
});
export const CrowdfundSubTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
letterSpacing: '1px',
fontWeight: 400,
fontSize: "17px",
fontSize: '17px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
borderBottom: `1px solid ${theme.palette.text.primary}`,
paddingBottom: "1.5px",
width: "fit-content",
textDecoration: "none",
paddingBottom: '1.5px',
width: 'fit-content',
textDecoration: 'none',
}));
export const CrowdfundDescription = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "16px",
fontSize: '16px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
}));
export const Spacer = ({ height }: any) => {
@@ -210,326 +201,313 @@ export const Spacer = ({ height }: any) => {
};
export const StyledCardHeaderComment = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "5px",
padding: "7px 0px",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '5px',
padding: '7px 0px',
});
export const StyledCardCol = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const StyledCardColComment = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const AuthorTextComment = styled(Typography)({
fontFamily: "Raleway, sans-serif",
fontSize: "16px",
lineHeight: "1.2",
fontSize: '16px',
lineHeight: '1.2',
});
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
color: "#fff",
height: "25px",
width: "auto",
color: '#fff',
height: '25px',
width: 'auto',
}));
export const CoverImagePreview = styled("img")(({ theme }) => ({
width: "100px",
height: "100px",
objectFit: "contain",
userSelect: "none",
borderRadius: "3px",
marginBottom: "10px",
export const CoverImagePreview = styled('img')(({ theme }) => ({
width: '100px',
height: '100px',
objectFit: 'contain',
userSelect: 'none',
borderRadius: '3px',
marginBottom: '10px',
}));
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "10px",
display: 'flex',
alignItems: 'center',
gap: '10px',
}));
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: "50%",
padding: "5px",
transition: "all 0.2s ease-in-out",
"&:hover": {
cursor: "pointer",
scale: "1.1",
borderRadius: '50%',
padding: '5px',
transition: 'all 0.2s ease-in-out',
'&:hover': {
cursor: 'pointer',
scale: '1.1',
},
}));
export const CrowdfundCardTitle = styled(DoubleLine)(({ theme }) => ({
fontFamily: "Montserrat",
fontSize: "24px",
letterSpacing: "-0.3px",
userSelect: "none",
marginBottom: "auto",
textAlign: "center",
"@media (max-width: 650px)": {
fontSize: "18px",
fontSize: '24px',
letterSpacing: '-0.3px',
userSelect: 'none',
marginBottom: 'auto',
textAlign: 'center',
'@media (max-width: 650px)': {
fontSize: '18px',
},
}));
export const CrowdfundUploadDate = styled(Typography)(({ theme }) => ({
fontFamily: "Montserrat",
fontSize: "12px",
letterSpacing: "0.2px",
fontSize: '12px',
letterSpacing: '0.2px',
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CATContainer = styled(Box)(({ theme }) => ({
position: "relative",
display: "flex",
padding: "15px",
flexDirection: "column",
gap: "20px",
justifyContent: "center",
width: "100%",
alignItems: "center",
position: 'relative',
display: 'flex',
padding: '15px',
flexDirection: 'column',
gap: '20px',
justifyContent: 'center',
width: '100%',
alignItems: 'center',
}));
export const AddCrowdFundButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
textTransform: "none",
padding: "10px 25px",
fontSize: "15px",
gap: "8px",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
textTransform: 'none',
padding: '10px 25px',
fontSize: '15px',
gap: '8px',
color: '#ffffff',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
border: "none",
borderRadius: "5px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
theme.palette.mode === 'dark' ? theme.palette.primary.main : '#2a9a86',
border: 'none',
borderRadius: '5px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
theme.palette.mode === 'dark' ? theme.palette.primary.dark : '#217e6d',
},
}));
export const EditCrowdFundButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
textTransform: "none",
padding: "5px 12px",
gap: "8px",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
textTransform: 'none',
padding: '5px 12px',
gap: '8px',
color: '#ffffff',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
border: "none",
borderRadius: "5px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
theme.palette.mode === 'dark' ? theme.palette.primary.main : '#2a9a86',
border: 'none',
borderRadius: '5px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
theme.palette.mode === 'dark' ? theme.palette.primary.dark : '#217e6d',
},
}));
export const CrowdfundListWrapper = styled(Box)(({ theme }) => ({
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "0px",
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: '0px',
background: theme.palette.background.default,
}));
export const CrowdfundTitleRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
gap: "10px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '10px',
}));
export const CrowdfundPageTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
fontSize: "35px",
fontSize: '35px',
fontWeight: 400,
letterSpacing: "1px",
userSelect: "none",
letterSpacing: '1px',
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundStatusRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "Mulish",
fontSize: "21px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '21px',
fontWeight: 400,
letterSpacing: 0,
border: `1px solid ${theme.palette.text.primary}`,
borderRadius: "8px",
padding: "15px 25px",
userSelect: "none",
borderRadius: '8px',
padding: '15px 25px',
userSelect: 'none',
}));
export const CrowdfundDescriptionRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontFamily: "Montserrat",
fontSize: "18px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
fontSize: '18px',
fontWeight: 400,
letterSpacing: 0,
});
export const AboutMyCrowdfund = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
fontSize: "23px",
fontSize: '23px',
fontWeight: 400,
letterSpacing: "1px",
userSelect: "none",
letterSpacing: '1px',
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundInlineContentRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
});
export const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
display: "flex",
fontFamily: "Mulish",
fontSize: "19px",
display: 'flex',
fontSize: '19px',
fontWeight: 400,
letterSpacing: 0,
userSelect: "none",
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundAccordion = styled(Accordion)(({ theme }) => ({
backgroundColor: theme.palette.primary.light,
"& .Mui-expanded": {
minHeight: "auto !important",
'& .Mui-expanded': {
minHeight: 'auto !important',
},
}));
export const CrowdfundAccordionSummary = styled(AccordionSummary)({
height: "50px",
"& .Mui-expanded": {
margin: "0px !important",
height: '50px',
'& .Mui-expanded': {
margin: '0px !important',
},
});
export const CrowdfundAccordionFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "20px",
fontSize: '20px',
fontWeight: 400,
letterSpacing: "0px",
letterSpacing: '0px',
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CrowdfundAccordionDetails = styled(AccordionDetails)({
padding: "0px 16px 16px 16px",
padding: '0px 16px 16px 16px',
});
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "16px",
display: 'flex',
alignItems: 'center',
fontSize: '16px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
}));
export const CoverImage = styled("img")({
width: "100%",
height: "250px",
objectFit: "cover",
objectPosition: "center",
export const CoverImage = styled('img')({
width: '100%',
height: '250px',
objectFit: 'cover',
objectPosition: 'center',
});
export const CrowdfundActionButtonRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
});
export const CrowdfundActionButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "16px",
display: 'flex',
alignItems: 'center',
fontSize: '16px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
}));
export const BackToHomeButton = styled(Button)(({ theme }) => ({
position: "absolute",
top: "20px",
left: "20px",
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "13px",
position: 'absolute',
top: '20px',
left: '20px',
display: 'flex',
alignItems: 'center',
fontSize: '13px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
padding: "5px 10px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
padding: '5px 10px',
backgroundColor: theme.palette.secondary.main,
transition: "all 0.3s ease-in-out",
"&:hover": {
transition: 'all 0.3s ease-in-out',
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
cursor: "pointer",
cursor: 'pointer',
},
}));
export const CrowdfundLoaderRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "10px",
padding: "10px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
padding: '10px',
});
export const RatingContainer = styled(Box)({
display: "flex",
alignItems: "center",
padding: "1px 5px",
borderRadius: "5px",
backgroundColor: "transparent",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
backgroundColor: "#e4ddddac",
display: 'flex',
alignItems: 'center',
padding: '1px 5px',
borderRadius: '5px',
backgroundColor: 'transparent',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor: '#e4ddddac',
},
});
export const StyledRating = styled(Rating)({
fontSize: "28px",
fontSize: '28px',
});
export const NoReviewsFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
fontWeight: 400,
letterSpacing: 0,
color: theme.palette.text.primary,
@@ -541,43 +519,40 @@ export const StyledButton = styled(Button)(({ theme }) => ({
}));
export const CustomSelect = styled(Select)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
"& .MuiSelect-select": {
padding: "12px",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiSelect-select': {
padding: '12px',
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
borderRadius: theme.shape.borderRadius, // Match border radius
},
"&:before": {
'&:before': {
// Underline style
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
borderBottomColor: theme.palette.mode === 'light' ? '#B2BAC2' : '#c9cccf',
},
"&:after": {
'&:after': {
// Underline style when focused
borderBottomColor: theme.palette.secondary.main,
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#E0E3E7',
},
"&:hover fieldset": {
borderColor: "#B2BAC2",
'&:hover fieldset': {
borderColor: '#B2BAC2',
},
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
'&.Mui-focused fieldset': {
borderColor: '#6F7E8C',
},
},
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiInputBase-root': {
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
},

View File

@@ -1,4 +1,4 @@
import { styled } from "@mui/system";
import { styled } from '@mui/system';
import {
Accordion,
AccordionDetails,
@@ -10,9 +10,9 @@ import {
TextField,
Typography,
Select,
} from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx";
} from '@mui/material';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { TimesSVG } from '../../../assets/svgs/TimesSVG.tsx';
export const DoubleLine = styled(Typography)`
display: -webkit-box;
@@ -22,181 +22,172 @@ export const DoubleLine = styled(Typography)`
`;
export const MainContainer = styled(Grid)({
width: "100%",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
width: '100%',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
margin: 0,
});
export const MainCol = styled(Grid)(({ theme }) => ({
display: "flex",
flexDirection: "column",
alignItems: "center",
width: "100%",
padding: "20px",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
padding: '20px',
}));
export const CreateContainer = styled(Box)(({ theme }) => ({
position: "fixed",
bottom: "20px",
right: "20px",
cursor: "pointer",
position: 'fixed',
bottom: '20px',
right: '20px',
cursor: 'pointer',
background: theme.palette.background.default,
width: "50px",
height: "50px",
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "50%",
width: '50px',
height: '50px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '50%',
}));
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
position: 'absolute',
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
borderRadius: '4px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '75%',
maxWidth: '900px',
padding: '15px 35px',
display: 'flex',
flexDirection: 'column',
gap: '17px',
overflowY: 'auto',
maxHeight: '95vh',
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
theme.palette.mode === 'dark'
? '0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)'
: 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px',
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
'&::-webkit-scrollbar-track:hover': {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
'&::-webkit-scrollbar': {
width: '16px',
height: '10px',
backgroundColor: theme.palette.mode === 'light' ? '#f6f8fa' : '#292d3e',
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.mode === 'light' ? '#d3d9e1' : '#575757',
borderRadius: '8px',
backgroundClip: 'content-box',
border: '4px solid transparent',
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme.palette.mode === 'light' ? '#b7bcc4' : '#474646',
},
}));
export const NewCrowdfundTitle = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "25px",
userSelect: "none",
fontSize: '25px',
userSelect: 'none',
}));
export const NewCrowdFundFont = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "18px",
userSelect: "none",
fontSize: '18px',
userSelect: 'none',
}));
export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "18px",
userSelect: "none",
fontStyle: "italic",
textDecoration: "underline",
fontSize: '18px',
userSelect: 'none',
fontStyle: 'italic',
textDecoration: 'underline',
}));
export const CustomInputField = styled(TextField)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
borderColor: theme.palette.background.paper,
"& label": {
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& label': {
color: theme.palette.mode === 'light' ? '#808183' : '#edeef0',
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
},
"& label.Mui-focused": {
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
'& label.Mui-focused': {
color: theme.palette.mode === 'light' ? '#A0AAB4' : '#d7d8da',
},
"& .MuiInput-underline:after": {
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
'& .MuiInput-underline:after': {
borderBottomColor: theme.palette.mode === 'light' ? '#B2BAC2' : '#c9cccf',
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#E0E3E7',
},
"&:hover fieldset": {
borderColor: "#B2BAC2",
'&:hover fieldset': {
borderColor: '#B2BAC2',
},
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
'&.Mui-focused fieldset': {
borderColor: '#6F7E8C',
},
},
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiInputBase-root': {
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
},
"& [class$='-MuiFilledInput-root']": {
padding: "30px 12px 8px",
padding: '30px 12px 8px',
},
"& .MuiFilledInput-root:after": {
'& .MuiFilledInput-root:after': {
borderBottomColor: theme.palette.secondary.main,
},
}));
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
letterSpacing: '1px',
fontWeight: 400,
fontSize: "20px",
fontSize: '20px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
}));
export const CrowdfundSubTitleRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
flexDirection: "row",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
flexDirection: 'row',
});
export const CrowdfundSubTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
letterSpacing: '1px',
fontWeight: 400,
fontSize: "17px",
fontSize: '17px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
borderBottom: `1px solid ${theme.palette.text.primary}`,
paddingBottom: "1.5px",
width: "fit-content",
textDecoration: "none",
paddingBottom: '1.5px',
width: 'fit-content',
textDecoration: 'none',
}));
export const CrowdfundDescription = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "16px",
fontSize: '16px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
}));
export const Spacer = ({ height }: any) => {
@@ -210,326 +201,313 @@ export const Spacer = ({ height }: any) => {
};
export const StyledCardHeaderComment = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "5px",
padding: "7px 0px",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '5px',
padding: '7px 0px',
});
export const StyledCardCol = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const StyledCardColComment = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const AuthorTextComment = styled(Typography)({
fontFamily: "Raleway, sans-serif",
fontSize: "16px",
lineHeight: "1.2",
fontSize: '16px',
lineHeight: '1.2',
});
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
color: "#fff",
height: "25px",
width: "auto",
color: '#fff',
height: '25px',
width: 'auto',
}));
export const CoverImagePreview = styled("img")(({ theme }) => ({
width: "100px",
height: "100px",
objectFit: "contain",
userSelect: "none",
borderRadius: "3px",
marginBottom: "10px",
export const CoverImagePreview = styled('img')(({ theme }) => ({
width: '100px',
height: '100px',
objectFit: 'contain',
userSelect: 'none',
borderRadius: '3px',
marginBottom: '10px',
}));
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "10px",
display: 'flex',
alignItems: 'center',
gap: '10px',
}));
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: "50%",
padding: "5px",
transition: "all 0.2s ease-in-out",
"&:hover": {
cursor: "pointer",
scale: "1.1",
borderRadius: '50%',
padding: '5px',
transition: 'all 0.2s ease-in-out',
'&:hover': {
cursor: 'pointer',
scale: '1.1',
},
}));
export const CrowdfundCardTitle = styled(DoubleLine)(({ theme }) => ({
fontFamily: "Montserrat",
fontSize: "24px",
letterSpacing: "-0.3px",
userSelect: "none",
marginBottom: "auto",
textAlign: "center",
"@media (max-width: 650px)": {
fontSize: "18px",
fontSize: '24px',
letterSpacing: '-0.3px',
userSelect: 'none',
marginBottom: 'auto',
textAlign: 'center',
'@media (max-width: 650px)': {
fontSize: '18px',
},
}));
export const CrowdfundUploadDate = styled(Typography)(({ theme }) => ({
fontFamily: "Montserrat",
fontSize: "12px",
letterSpacing: "0.2px",
fontSize: '12px',
letterSpacing: '0.2px',
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CATContainer = styled(Box)(({ theme }) => ({
position: "relative",
display: "flex",
padding: "15px",
flexDirection: "column",
gap: "20px",
justifyContent: "center",
width: "100%",
alignItems: "center",
position: 'relative',
display: 'flex',
padding: '15px',
flexDirection: 'column',
gap: '20px',
justifyContent: 'center',
width: '100%',
alignItems: 'center',
}));
export const AddCrowdFundButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
textTransform: "none",
padding: "10px 25px",
fontSize: "15px",
gap: "8px",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
textTransform: 'none',
padding: '10px 25px',
fontSize: '15px',
gap: '8px',
color: '#ffffff',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
border: "none",
borderRadius: "5px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
theme.palette.mode === 'dark' ? theme.palette.primary.main : '#2a9a86',
border: 'none',
borderRadius: '5px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
theme.palette.mode === 'dark' ? theme.palette.primary.dark : '#217e6d',
},
}));
export const EditCrowdFundButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
textTransform: "none",
padding: "5px 12px",
gap: "8px",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
textTransform: 'none',
padding: '5px 12px',
gap: '8px',
color: '#ffffff',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
border: "none",
borderRadius: "5px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
theme.palette.mode === 'dark' ? theme.palette.primary.main : '#2a9a86',
border: 'none',
borderRadius: '5px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
theme.palette.mode === 'dark' ? theme.palette.primary.dark : '#217e6d',
},
}));
export const CrowdfundListWrapper = styled(Box)(({ theme }) => ({
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "0px",
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: '0px',
background: theme.palette.background.default,
}));
export const CrowdfundTitleRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
gap: "10px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '10px',
}));
export const CrowdfundPageTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
fontSize: "35px",
fontSize: '35px',
fontWeight: 400,
letterSpacing: "1px",
userSelect: "none",
letterSpacing: '1px',
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundStatusRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "Mulish",
fontSize: "21px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '21px',
fontWeight: 400,
letterSpacing: 0,
border: `1px solid ${theme.palette.text.primary}`,
borderRadius: "8px",
padding: "15px 25px",
userSelect: "none",
borderRadius: '8px',
padding: '15px 25px',
userSelect: 'none',
}));
export const CrowdfundDescriptionRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontFamily: "Montserrat",
fontSize: "18px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
fontSize: '18px',
fontWeight: 400,
letterSpacing: 0,
});
export const AboutMyCrowdfund = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
fontSize: "23px",
fontSize: '23px',
fontWeight: 400,
letterSpacing: "1px",
userSelect: "none",
letterSpacing: '1px',
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundInlineContentRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
});
export const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
display: "flex",
fontFamily: "Mulish",
fontSize: "19px",
display: 'flex',
fontSize: '19px',
fontWeight: 400,
letterSpacing: 0,
userSelect: "none",
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundAccordion = styled(Accordion)(({ theme }) => ({
backgroundColor: theme.palette.primary.light,
"& .Mui-expanded": {
minHeight: "auto !important",
'& .Mui-expanded': {
minHeight: 'auto !important',
},
}));
export const CrowdfundAccordionSummary = styled(AccordionSummary)({
height: "50px",
"& .Mui-expanded": {
margin: "0px !important",
height: '50px',
'& .Mui-expanded': {
margin: '0px !important',
},
});
export const CrowdfundAccordionFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "20px",
fontSize: '20px',
fontWeight: 400,
letterSpacing: "0px",
letterSpacing: '0px',
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CrowdfundAccordionDetails = styled(AccordionDetails)({
padding: "0px 16px 16px 16px",
padding: '0px 16px 16px 16px',
});
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "16px",
display: 'flex',
alignItems: 'center',
fontSize: '16px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
}));
export const CoverImage = styled("img")({
width: "100%",
height: "250px",
objectFit: "cover",
objectPosition: "center",
export const CoverImage = styled('img')({
width: '100%',
height: '250px',
objectFit: 'cover',
objectPosition: 'center',
});
export const CrowdfundActionButtonRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
});
export const CrowdfundActionButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "16px",
display: 'flex',
alignItems: 'center',
fontSize: '16px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
}));
export const BackToHomeButton = styled(Button)(({ theme }) => ({
position: "absolute",
top: "20px",
left: "20px",
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "13px",
position: 'absolute',
top: '20px',
left: '20px',
display: 'flex',
alignItems: 'center',
fontSize: '13px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
padding: "5px 10px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
padding: '5px 10px',
backgroundColor: theme.palette.secondary.main,
transition: "all 0.3s ease-in-out",
"&:hover": {
transition: 'all 0.3s ease-in-out',
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
cursor: "pointer",
cursor: 'pointer',
},
}));
export const CrowdfundLoaderRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "10px",
padding: "10px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
padding: '10px',
});
export const RatingContainer = styled(Box)({
display: "flex",
alignItems: "center",
padding: "1px 5px",
borderRadius: "5px",
backgroundColor: "transparent",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
backgroundColor: "#e4ddddac",
display: 'flex',
alignItems: 'center',
padding: '1px 5px',
borderRadius: '5px',
backgroundColor: 'transparent',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor: '#e4ddddac',
},
});
export const StyledRating = styled(Rating)({
fontSize: "28px",
fontSize: '28px',
});
export const NoReviewsFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
fontWeight: 400,
letterSpacing: 0,
color: theme.palette.text.primary,
@@ -541,43 +519,40 @@ export const StyledButton = styled(Button)(({ theme }) => ({
}));
export const CustomSelect = styled(Select)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
"& .MuiSelect-select": {
padding: "12px",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiSelect-select': {
padding: '12px',
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
borderRadius: theme.shape.borderRadius, // Match border radius
},
"&:before": {
'&:before': {
// Underline style
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
borderBottomColor: theme.palette.mode === 'light' ? '#B2BAC2' : '#c9cccf',
},
"&:after": {
'&:after': {
// Underline style when focused
borderBottomColor: theme.palette.secondary.main,
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#E0E3E7',
},
"&:hover fieldset": {
borderColor: "#B2BAC2",
'&:hover fieldset': {
borderColor: '#B2BAC2',
},
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
'&.Mui-focused fieldset': {
borderColor: '#6F7E8C',
},
},
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiInputBase-root': {
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
},

View File

@@ -10,36 +10,29 @@ import {
SelectChangeEvent,
Typography,
useTheme,
} from "@mui/material";
import { Signal, useSignal, useSignalEffect } from "@preact/signals-react";
import Compressor from "compressorjs";
import React, { useEffect, useState } from "react";
import { useDropzone } from "react-dropzone";
import { useDispatch, useSelector } from "react-redux";
import ShortUniqueId from "short-unique-id";
import { categories, subCategories } from "../../../constants/Categories.ts";
import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts";
} from '@mui/material';
import Compressor from 'compressorjs';
import React, { useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { categories, subCategories } from '../../../constants/Categories.ts';
import { QTUBE_VIDEO_BASE } from '../../../constants/Identifiers.ts';
import {
maxSize,
titleFormatter,
videoMaxSize,
} from "../../../constants/Misc.ts";
} from '../../../constants/Misc.ts';
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import BoundedNumericTextField from '../../../utils/BoundedNumericTextField.tsx';
import { objectToBase64 } from '../../../utils/PublishFormatter.ts';
import {
setEditVideo,
updateInHashMap,
updateVideo,
} from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts";
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx";
import { objectToBase64 } from "../../../utils/PublishFormatter.ts";
import { FrameExtractor } from "../../common/FrameExtractor/FrameExtractor.tsx";
import ImageUploader from "../../common/ImageUploader.tsx";
import { TextEditor } from "../../common/TextEditor/TextEditor.tsx";
import { extractTextFromHTML } from "../../common/TextEditor/utils.ts";
import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll.tsx";
import { toBase64 } from "../PublishVideo/PublishVideo.tsx";
getFileExtension,
getFileExtensionIndex,
} from '../../../utils/stringFunctions.ts';
import { FrameExtractor } from '../../common/FrameExtractor/FrameExtractor.tsx';
import ImageUploader from '../../common/ImageUploader.tsx';
import { TextEditor } from '../../common/TextEditor/TextEditor.tsx';
import { extractTextFromHTML } from '../../common/TextEditor/utils.ts';
import { toBase64 } from '../PublishVideo/PublishVideo.tsx';
import {
AddCoverImageButton,
@@ -52,136 +45,102 @@ import {
ModalBody,
NewCrowdfundTitle,
TimesIcon,
} from "./EditVideo-styles.tsx";
const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 });
} from './EditVideo-styles.tsx';
import { showError, useAuth, useGlobal, usePublish } from 'qapp-core';
import { useAtom, useSetAtom } from 'jotai';
import {
AltertObject,
setNotificationAtom,
} from '../../../state/global/notifications.ts';
import { editVideoAtom } from '../../../state/publish/video.ts';
import { useMediaInfo } from '../../../hooks/useMediaInfo.tsx';
export const EditVideo = () => {
const theme = useTheme();
const dispatch = useDispatch();
const username = useSelector((state: RootState) => state.auth?.user?.name);
const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address
);
const editVideoProperties = useSelector(
(state: RootState) => state.video.editVideoProperties
);
const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
useState(null);
const setNotification = useSetAtom(setNotificationAtom);
const setEditVideo = useSetAtom(editVideoAtom);
const { name: username, address: userAddress } = useAuth();
const { lists } = useGlobal();
const [editVideoProperties] = useAtom(editVideoAtom);
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [coverImage, setCoverImage] = useState<string>("");
const [file, setFile] = useState(null);
const publishFromLibrary = usePublish();
const [title, setTitle] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [coverImage, setCoverImage] = useState<string>('');
const [file, setFile] = useState<null | File>(null);
const [selectedCategoryVideos, setSelectedCategoryVideos] =
useState<any>(null);
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
useState<any>(null);
const [imageExtracts, setImageExtracts] = useState<any>([]);
const videoDuration: Signal<number[]> = useSignal([
const [videoDurations, setVideoDurations] = useState<number[]>([
editVideoProperties?.duration || 0,
]);
const { isHEVC } = useMediaInfo();
useEffect(() => {
videoDuration.value[0] = Math.floor(editVideoProperties?.duration);
}, [editVideoProperties]);
if (editVideoProperties?.duration) {
setVideoDurations([Math.floor(editVideoProperties?.duration || 0)]);
}
}, [editVideoProperties?.duration]);
const { getRootProps, getInputProps } = useDropzone({
accept: {
"video/*": [],
'video/*': [],
},
maxFiles: 1,
maxSize,
onDrop: (acceptedFiles, rejectedFiles) => {
onDrop: async (acceptedFiles, rejectedFiles) => {
const firstFile = acceptedFiles[0];
setFile(firstFile);
const notSupportedCodec = await isHEVC(firstFile);
let errorString = null;
const isMKV = getFileExtension(firstFile) === 'mkv';
const isUnsupportedFile = notSupportedCodec || isMKV;
if (isUnsupportedFile) {
if (notSupportedCodec)
showError(`${firstFile.name} uses the unsupported encoding: HEVC`);
if (isMKV)
showError(
`${firstFile.name} uses the unsupported file container: MKV`
);
} else setFile(firstFile);
let errorString: null | string = null;
rejectedFiles.forEach(({ file, errors }) => {
errors.forEach(error => {
if (error.code === "file-too-large") {
errors.forEach((error) => {
if (error.code === 'file-too-large') {
errorString = `File must be under ${videoMaxSize}MB`;
}
console.log(`Error with file ${file.name}: ${error.message}`);
console.error(`Error with file ${file.name}: ${error.message}`);
});
});
if (errorString) {
const notificationObj = {
const notificationObj: AltertObject = {
msg: errorString,
alertType: "error",
alertType: 'error',
};
dispatch(setNotification(notificationObj));
setNotification(notificationObj);
}
},
});
// useEffect(() => {
// if (editVideoProperties) {
// const descriptionString = editVideoProperties?.description || "";
// // Splitting the string at the asterisks
// const parts = descriptionString.split("**");
// // The part within the asterisks
// const extractedString = parts[1];
// // The part after the last asterisks
// const description = parts[2] || ""; // Using '|| '' to handle cases where there is no text after the last **
// setTitle(editVideoProperties?.title || "");
// setDescription(editVideoProperties?.fullDescription || "");
// setCoverImage(editVideoProperties?.videoImage || "");
// // Split the extracted string into key-value pairs
// const keyValuePairs = extractedString.split(";");
// // Initialize variables to hold the category and subcategory values
// let category, subcategory;
// // Loop through each key-value pair
// keyValuePairs.forEach((pair) => {
// const [key, value] = pair.split(":");
// // Check the key and assign the value to the appropriate variable
// if (key === "category") {
// category = value;
// } else if (key === "subcategory") {
// subcategory = value;
// }
// });
// if(category){
// const selectedOption = categories.find((option) => option.id === +category);
// setSelectedCategoryVideos(selectedOption || null);
// }
// if(subcategory){
// const selectedOption = categories.find((option) => option.id === +subcategory);
// setSelectedCategoryVideos(selectedOption || null);
// }
// }
// }, [editVideoProperties]);
useEffect(() => {
if (editVideoProperties) {
setTitle(editVideoProperties?.title || "");
setTitle(editVideoProperties?.title || '');
if (editVideoProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription);
} else if (editVideoProperties?.fullDescription) {
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`;
setDescription(paragraph);
}
setCoverImage(editVideoProperties?.videoImage || "");
setCoverImage(editVideoProperties?.videoImage || '');
if (editVideoProperties?.category) {
const selectedOption = categories.find(
option => option.id === +editVideoProperties.category
(option) => option.id === +editVideoProperties.category
);
setSelectedCategoryVideos(selectedOption || null);
}
@@ -193,38 +152,38 @@ export const EditVideo = () => {
) {
const selectedOption = subCategories[
+editVideoProperties?.category
]?.find(option => option.id === +editVideoProperties.subcategory);
]?.find((option) => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null);
}
}
}, [editVideoProperties]);
const onClose = () => {
dispatch(setEditVideo(null));
setVideoPropertiesToSetToRedux(null);
setEditVideo(null);
setFile(null);
setTitle("");
setTitle('');
setImageExtracts([]);
setDescription("");
setCoverImage("");
setDescription('');
setCoverImage('');
};
async function publishQDNResource() {
try {
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!coverImage) throw new Error("Please select cover image");
if (!selectedCategoryVideos) throw new Error("Please select a category");
if (!username) throw new Error('A name is required to publish');
if (!title) throw new Error('Please enter a title');
if (!description) throw new Error('Please enter a description');
if (!coverImage) throw new Error('Please select cover image');
if (!selectedCategoryVideos) throw new Error('Please select a category');
if (!editVideoProperties) return;
if (!userAddress) throw new Error("Unable to locate user address");
let errorMsg = "";
let name = "";
if (!userAddress) throw new Error('Unable to locate user address');
let errorMsg = '';
let name = '';
if (username) {
name = username;
}
if (!name) {
errorMsg =
"Cannot publish without access to your name. Please authenticate.";
'Cannot publish without access to your name. Please authenticate.';
}
if (editVideoProperties?.user !== username) {
@@ -232,35 +191,34 @@ export const EditVideo = () => {
}
if (errorMsg) {
dispatch(
setNotification({
msg: errorMsg,
alertType: "error",
})
);
const notificationObj: AltertObject = {
msg: errorMsg,
alertType: 'error',
};
setNotification(notificationObj);
return;
}
const listOfPublishes = [];
const listOfPublishes: any[] = [];
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
const subcategory = selectedSubCategoryVideos?.id || '';
const fullDescription = extractTextFromHTML(description);
let fileExtension = "mp4";
const fileExtensionSplit = file?.name?.split(".");
if (fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || "mp4";
let fileExtension = 'mp4';
const fileExtensionSplit = file?.name?.split('.');
if (fileExtensionSplit && fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || 'mp4';
}
const filename = title.slice(0, 15);
// Step 1: Replace all white spaces with underscores
// Replace all forms of whitespace (including non-standard ones) with underscores
const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, '_');
// Remove all non-alphanumeric characters (except underscores)
const alphanumericString = stringWithUnderscores.replace(
/[^a-zA-Z0-9_]/g,
""
''
);
const videoObject: any = {
@@ -275,21 +233,20 @@ export const EditVideo = () => {
category,
subcategory,
code: editVideoProperties.code,
videoType: file?.type || "video/mp4",
videoType: file?.type || editVideoProperties?.videoType || 'video/mp4',
filename: `${alphanumericString.trim()}.${fileExtension}`,
fileSize: file?.size || 0,
duration: videoDuration.value[0] || editVideoProperties?.duration || 0,
fileSize: file?.size || editVideoProperties?.fileSize || 0,
duration: videoDurations[0] || editVideoProperties?.duration || 0,
};
console.log("edit publish duration: ", videoObject?.duration);
const metadescription =
`**category:${category};subcategory:${subcategory};code:${editVideoProperties.code}**` +
description.slice(0, 150);
// Description is obtained from raw data
const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE",
action: 'PUBLISH_QDN_RESOURCE',
name: username,
service: "DOCUMENT",
service: 'DOCUMENT',
data64: await objectToBase64(videoObject),
title: title.slice(0, 50),
description: metadescription,
@@ -301,51 +258,55 @@ export const EditVideo = () => {
if (file && editVideoProperties.videoReference?.identifier) {
const requestBodyVideo: any = {
action: "PUBLISH_QDN_RESOURCE",
action: 'PUBLISH_QDN_RESOURCE',
name: username,
service: "VIDEO",
service: 'VIDEO',
file,
title: title.slice(0, 50),
description: metadescription,
identifier: editVideoProperties.videoReference?.identifier,
tag1: QTUBE_VIDEO_BASE,
filename: `${alphanumericString.trim()}.${fileExtension}`,
filename: file.name,
};
listOfPublishes.push(requestBodyVideo);
}
const multiplePublish = {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [...listOfPublishes],
};
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
setVideoPropertiesToSetToRedux({
...editVideoProperties,
...videoObject,
});
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {
notificationObj = {
msg: error || "Failed to publish update",
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || "Failed to publish update",
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || "Failed to publish update",
alertType: "error",
};
}
if (!notificationObj) return;
dispatch(setNotification(notificationObj));
await publishFromLibrary.publishMultipleResources(listOfPublishes);
throw new Error("Failed to publish update");
lists.updateNewResources([
{
data: videoObject,
qortalMetadata: {
identifier: editVideoProperties.id,
service: 'DOCUMENT',
name: username,
size: 100,
updated: Date.now(),
metadata: {
title: title.slice(0, 50),
description: metadescription,
tags: [QTUBE_VIDEO_BASE],
},
created: editVideoProperties?.created,
},
},
]);
const notificationObj: AltertObject = {
msg: 'Video updated',
alertType: 'success',
};
setNotification(notificationObj);
onClose();
} catch (error: any) {
const isError = error instanceof Error;
const message = isError ? error?.message : 'Failed to publish update';
const notificationObj: AltertObject = {
msg: message,
alertType: 'error',
};
setNotification(notificationObj);
throw new Error('Failed to publish update');
}
}
@@ -353,7 +314,7 @@ export const EditVideo = () => {
event: SelectChangeEvent<string>
) => {
const optionId = event.target.value;
const selectedOption = categories.find(option => option.id === +optionId);
const selectedOption = categories.find((option) => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos = (
@@ -362,39 +323,42 @@ export const EditVideo = () => {
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
option => option.id === +optionId
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos(selectedOption || null);
};
const onFramesExtracted = async imgs => {
const onFramesExtracted = async (imgs) => {
try {
const imagesExtracts = [];
const imagesExtracts: string[] = [];
for (const img of imgs) {
try {
let compressedFile;
const image = img;
await new Promise<void>(resolve => {
await new Promise<void>((resolve) => {
new Compressor(image, {
quality: 0.8,
maxWidth: 750,
mimeType: "image/webp",
mimeType: 'image/webp',
success(result) {
const file = new File([result], "name", {
type: "image/webp",
const file = new File([result], 'name', {
type: 'image/webp',
});
compressedFile = file;
resolve();
},
error(error) {
console.log(error);
console.error(error);
},
});
});
if (!compressedFile) continue;
const base64Img = await toBase64(compressedFile);
imagesExtracts.push(base64Img);
const result = await toBase64(compressedFile);
if (result && typeof result === 'string') {
imagesExtracts.push(result);
}
} catch (error) {
console.error(error);
}
@@ -402,7 +366,7 @@ export const EditVideo = () => {
setImageExtracts(imagesExtracts);
} catch (error) {
console.log(error);
console.error(error);
}
};
@@ -413,12 +377,12 @@ export const EditVideo = () => {
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody sx={{ maxHeight: "98vh" }}>
<ModalBody sx={{ maxHeight: '98vh' }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<NewCrowdfundTitle>Update Video properties</NewCrowdfundTitle>
@@ -427,11 +391,11 @@ export const EditVideo = () => {
<Box
{...getRootProps()}
sx={{
border: "1px dashed gray",
border: '1px dashed gray',
padding: 2,
textAlign: "center",
textAlign: 'center',
marginBottom: 2,
cursor: "pointer",
cursor: 'pointer',
}}
>
<input {...getInputProps()} />
@@ -439,16 +403,16 @@ export const EditVideo = () => {
</Box>
<Typography
sx={{
marginBottom: "10px",
marginBottom: '10px',
}}
>
{file?.name}
</Typography>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
display: 'flex',
gap: '20px',
alignItems: 'center',
}}
>
<FormControl fullWidth sx={{ marginBottom: 2 }}>
@@ -456,10 +420,10 @@ export const EditVideo = () => {
<Select
labelId="Category"
input={<OutlinedInput label="Select a Category" />}
value={selectedCategoryVideos?.id || ""}
value={selectedCategoryVideos?.id || ''}
onChange={handleOptionCategoryChangeVideos}
>
{categories.map(option => (
{categories.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
@@ -473,19 +437,21 @@ export const EditVideo = () => {
<Select
labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={e =>
value={selectedSubCategoryVideos?.id || ''}
onChange={(e) =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
>
{subCategories[selectedCategoryVideos.id].map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
{subCategories[selectedCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
@@ -493,8 +459,9 @@ export const EditVideo = () => {
{file && (
<FrameExtractor
videoFile={file}
onFramesExtracted={imgs => onFramesExtracted(imgs)}
videoDurations={videoDuration}
onFramesExtracted={(imgs) => onFramesExtracted(imgs)}
videoDurations={videoDurations}
setVideoDurations={setVideoDurations}
index={0}
/>
)}
@@ -505,8 +472,8 @@ export const EditVideo = () => {
Add Cover Image
<AddLogoIcon
sx={{
height: "25px",
width: "auto",
height: '25px',
width: 'auto',
}}
></AddLogoIcon>
</AddCoverImageButton>
@@ -516,9 +483,9 @@ export const EditVideo = () => {
<CoverImagePreview src={coverImage} alt="logo" />
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() => setCoverImage("")}
height={"32"}
width={"32"}
onClickFunc={() => setCoverImage('')}
height={'32'}
width={'32'}
></TimesIcon>
</LogoPreviewRow>
)}
@@ -528,9 +495,10 @@ export const EditVideo = () => {
label="Video Duration in Seconds"
addIconButtons={false}
allowDecimals={false}
initialValue={videoDuration.value[0].toString()}
afterChange={s => {
videoDuration.value[0] = +s;
initialValue={videoDurations[0].toString()}
afterChange={(s) => {
const newS = +s;
setVideoDurations([newS]);
}}
/>
<CustomInputField
@@ -538,9 +506,9 @@ export const EditVideo = () => {
label="Title of video"
variant="filled"
value={title}
onChange={e => {
onChange={(e) => {
const value = e.target.value;
const formattedValue = value.replace(titleFormatter, "");
const formattedValue = value.replace(titleFormatter, '');
setTitle(formattedValue);
}}
inputProps={{ maxLength: 180 }}
@@ -548,28 +516,17 @@ export const EditVideo = () => {
/>
<Typography
sx={{
fontSize: "18px",
fontSize: '18px',
}}
>
Description of video
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setInlineContent={(value) => {
setDescription(value);
}}
/>
{/* <CustomInputField
name="description"
label="Describe your video in a few words"
variant="filled"
value={description}
onChange={(e) => setDescription(e.target.value)}
inputProps={{ maxLength: 10000 }}
multiline
maxRows={3}
required
/> */}
</React.Fragment>
</>
@@ -585,9 +542,9 @@ export const EditVideo = () => {
</CrowdfundActionButton>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
display: 'flex',
gap: '20px',
alignItems: 'center',
}}
>
<CrowdfundActionButton
@@ -595,7 +552,7 @@ export const EditVideo = () => {
onClick={() => {
publishQDNResource();
}}
disabled={file && imageExtracts.length === 0}
disabled={!!file && imageExtracts?.length === 0}
>
{file && imageExtracts.length === 0 && (
<CircularProgress color="secondary" size={14} />
@@ -606,37 +563,6 @@ export const EditVideo = () => {
</CrowdfundActionButtonRow>
</ModalBody>
</Modal>
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onError={messageNotification => {
setIsOpenMultiplePublish(false);
setPublishes(null);
if (messageNotification) {
dispatch(
setNotification({
msg: messageNotification,
alertType: "error",
})
);
}
}}
onSubmit={() => {
setIsOpenMultiplePublish(false);
const clonedCopy = structuredClone(videoPropertiesToSetToRedux);
dispatch(updateVideo(clonedCopy));
dispatch(updateInHashMap(clonedCopy));
dispatch(
setNotification({
msg: "Video updated",
alertType: "success",
})
);
onClose();
}}
publishes={publishes}
/>
)}
</>
);
};

View File

@@ -11,6 +11,8 @@ import React, { useCallback, useEffect, useState, useRef } from "react";
import { CircleSVG } from "../../../assets/svgs/CircleSVG.tsx";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG.tsx";
import { styled } from "@mui/system";
import { useAtomValue } from "jotai";
import { usePublish } from "qapp-core";
interface Publish {
resources: any[];
@@ -30,6 +32,7 @@ export const MultiplePublish = ({
onError,
}: MultiplePublishProps) => {
const theme = useTheme();
const publishFromLibrary = usePublish()
const listOfSuccessfulPublishesRef = useRef([]);
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
@@ -41,7 +44,7 @@ export const MultiplePublish = ({
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length;
const lengthOfTimeout = lengthOfResources * 1200000; // Time out in QR, Seconds = 20 Minutes
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
return await publishFromLibrary.publishMultipleResources(pub.resources)
}, []);
const [isPublishing, setIsPublishing] = useState(true);
@@ -123,9 +126,12 @@ export const MultiplePublish = ({
const unpublished = listOfUnsuccessfulPublishes.map(
item => item?.identifier
);
const key = `${publish?.service}-${publish?.name}-${publish?.identifier}`
return (
<Box key={key}>
<Box
key={publish?.identifier}
sx={{
display: "flex",
gap: "20px",
@@ -154,6 +160,8 @@ export const MultiplePublish = ({
<CircularProgress size={16} color="secondary" />
)}
</Box>
</Box>
);
})}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx";
import { useState } from 'react';
import { CardContentContainerComment } from '../../common/Comments/Comments-styles.tsx';
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../PublishVideo/PublishVideo-styles.tsx";
} from '../PublishVideo/PublishVideo-styles.tsx';
import {
Box,
Button,
@@ -14,28 +14,29 @@ import {
Typography,
useTheme,
FormControlLabel,
} from "@mui/material";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { useNavigate } from "react-router-dom";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { removeVideo } from "../../../state/features/videoSlice.ts";
import AddIcon from "@mui/icons-material/Add";
import { useSelector } from "react-redux";
import { RootState } from "../../../state/store.ts";
import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts";
} from '@mui/material';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import AddIcon from '@mui/icons-material/Add';
import { QTUBE_VIDEO_BASE } from '../../../constants/Identifiers.ts';
import { QortalMetadata, Spacer, useAuth } from 'qapp-core';
import { useIsSmall } from '../../../hooks/useIsSmall.tsx';
import { useTranslation } from 'react-i18next';
export const PlaylistListEdit = ({
playlistData,
updateVideoList,
removeVideo,
addVideo,
}) => {
const theme = useTheme();
const navigate = useNavigate();
const username = useSelector((state: RootState) => state.auth?.user?.name);
const { t } = useTranslation(['core']);
const [searchResults, setSearchResults] = useState([]);
const [filterSearch, setFilterSearch] = useState("");
const theme = useTheme();
const isSmall = useIsSmall();
const { name: username } = useAuth();
const [searchResults, setSearchResults] = useState<QortalMetadata[]>([]);
const [filterSearch, setFilterSearch] = useState('');
const [userSearch, setUserSearch] = useState(
`name=${username}&exactmatchnames=true&`
);
@@ -43,12 +44,12 @@ export const PlaylistListEdit = ({
const videos = playlistData?.videos || [];
//const [hoveredIndex, setHoveredIndex] = useState(null); // Mayb in the future
const handleRadioChange = event => {
const handleRadioChange = (event) => {
const value = event.target.value;
if (value === "myVideos") {
if (value === 'myVideos') {
setUserSearch(`name=${username}&exactmatchnames=true&`);
} else {
setUserSearch(""); // All videos
setUserSearch(''); // All videos
}
};
@@ -56,9 +57,9 @@ export const PlaylistListEdit = ({
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QTUBE_VIDEO_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&${userSearch}offset=0`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseDataSearchVid = await response.json();
@@ -81,27 +82,35 @@ export const PlaylistListEdit = ({
return (
<Box
sx={{
display: "flex",
gap: "10px",
width: "100%",
justifyContent: "center",
display: 'flex',
flexDirection: isSmall ? 'column' : 'row',
gap: '10px',
width: '100%',
justifyContent: 'center',
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
display: 'flex',
flexDirection: 'column',
flex: '1 1 50%',
outline: `1px ${theme.palette.action.active} solid`,
padding: '5px',
borderRadius: '5px',
}}
>
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
<CrowdfundSubTitle>
{t('core:publish.playlist', {
postProcess: 'capitalizeFirstChar',
})}
</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
sx={{
marginTop: "25px",
height: "450px",
overflow: "auto",
marginTop: '25px',
maxHeight: '450px',
overflow: 'auto',
}}
>
{videos.map((vid, index) => {
@@ -109,30 +118,30 @@ export const PlaylistListEdit = ({
<Box
key={vid?.identifier}
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
"&:hover .action-icons": { display: "flex" },
display: 'flex',
gap: '10px',
width: '100%',
alignItems: 'center',
padding: '10px',
borderRadius: '5px',
userSelect: 'none',
'&:hover .action-icons': { display: 'flex' },
}}
// onMouseEnter={() => setHoveredIndex(index)}
// onMouseLeave={() => setHoveredIndex(null)}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "2px",
display: 'flex',
flexDirection: 'column',
gap: '2px',
}}
>
<IconButton
size="small"
onClick={() => moveItem(index, -1)}
disabled={index === 0}
sx={{ padding: "2px" }}
sx={{ padding: '2px' }}
>
<ArrowUpwardIcon fontSize="small" />
</IconButton>
@@ -140,22 +149,22 @@ export const PlaylistListEdit = ({
size="small"
onClick={() => moveItem(index, 1)}
disabled={index === playlistData?.videos?.length - 1}
sx={{ padding: "2px" }}
sx={{ padding: '2px' }}
>
<ArrowDownwardIcon fontSize="small" />
</IconButton>
</Box>
<Typography
sx={{
fontSize: "14px",
fontSize: '14px',
}}
>
{index + 1}
</Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
fontSize: '18px',
wordBreak: 'break-word',
}}
>
{vid?.metadata?.title}
@@ -165,7 +174,7 @@ export const PlaylistListEdit = ({
removeVideo(index);
}}
sx={{
cursor: "pointer",
cursor: 'pointer',
}}
/>
</Box>
@@ -175,19 +184,26 @@ export const PlaylistListEdit = ({
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
display: 'flex',
flexDirection: 'column',
flex: '1 1 50%',
outline: `1px ${theme.palette.action.active} solid`,
padding: '5px',
borderRadius: '5px',
}}
>
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
<CrowdfundSubTitle>
{t('core:publish.add_videos_playlist', {
postProcess: 'capitalizeFirstChar',
})}
</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
sx={{
marginTop: "25px",
height: "450px",
overflow: "auto",
marginTop: '25px',
maxHeight: '450px',
overflow: 'auto',
}}
>
<Box>
@@ -200,63 +216,72 @@ export const PlaylistListEdit = ({
<FormControlLabel
value="myVideos"
control={<Radio />}
label="My Videos"
label={t('core:publish.my_videos', {
postProcess: 'capitalizeFirstChar',
})}
componentsProps={{
typography: { sx: { fontSize: "14px" } },
typography: { sx: { fontSize: '14px' } },
}}
/>
<FormControlLabel
value="allVideos"
control={<Radio />}
label="All Videos"
label={t('core:publish.all_videos', {
postProcess: 'capitalizeFirstChar',
})}
componentsProps={{
typography: { sx: { fontSize: "14px" } },
typography: { sx: { fontSize: '14px' } },
}}
/>
</RadioGroup>
</Box>
<Box
sx={{
display: "flex",
gap: "10px",
display: 'flex',
gap: '10px',
}}
>
<Input
id="standard-adornment-name"
onChange={e => {
onChange={(e) => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
placeholder="Search by title"
placeholder={t('core:publish.search_by_title', {
postProcess: 'capitalizeFirstChar',
})}
sx={{
borderBottom: "1px solid white",
"&&:before": {
borderBottom: "none",
borderBottom: '1px solid white',
'&&:before': {
borderBottom: 'none',
},
"&&:after": {
borderBottom: "none",
'&&:after': {
borderBottom: 'none',
},
"&&:hover:before": {
borderBottom: "none",
'&&:hover:before': {
borderBottom: 'none',
},
"&&.Mui-focused:before": {
borderBottom: "none",
'&&.Mui-focused:before': {
borderBottom: 'none',
},
"&&.Mui-focused": {
outline: "none",
'&&.Mui-focused': {
outline: 'none',
},
fontSize: "18px",
fontSize: '18px',
}}
/>
</Box>
<Box>
<Spacer height="20px" />
<Button
onClick={() => {
search();
}}
variant="contained"
>
Search
{t('core:navbar.search', {
postProcess: 'capitalizeFirstChar',
})}
</Button>
</Box>
@@ -265,26 +290,26 @@ export const PlaylistListEdit = ({
<Box
key={vid?.identifier}
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
display: 'flex',
gap: '10px',
width: '100%',
alignItems: 'center',
padding: '10px',
borderRadius: '5px',
userSelect: 'none',
}}
>
<Typography
sx={{
fontSize: "14px",
fontSize: '14px',
}}
>
{index + 1}
</Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
fontSize: '18px',
wordBreak: 'break-word',
}}
>
{vid?.metadata?.title}
@@ -294,7 +319,7 @@ export const PlaylistListEdit = ({
addVideo(vid);
}}
sx={{
cursor: "pointer",
cursor: 'pointer',
}}
/>
</Box>

View File

@@ -1,4 +1,4 @@
import { styled } from "@mui/system";
import { styled } from '@mui/system';
import {
Accordion,
AccordionDetails,
@@ -10,9 +10,9 @@ import {
TextField,
Typography,
Select,
} from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx";
} from '@mui/material';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { TimesSVG } from '../../../assets/svgs/TimesSVG.tsx';
export const DoubleLine = styled(Typography)`
display: -webkit-box;
@@ -22,183 +22,174 @@ export const DoubleLine = styled(Typography)`
`;
export const MainContainer = styled(Grid)({
width: "100%",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
width: '100%',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
margin: 0,
});
export const MainCol = styled(Grid)(({ theme }) => ({
display: "flex",
flexDirection: "column",
alignItems: "center",
width: "100%",
padding: "20px",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
padding: '20px',
}));
export const CreateContainer = styled(Box)(({ theme }) => ({
position: "fixed",
bottom: "20px",
right: "20px",
cursor: "pointer",
position: 'fixed',
bottom: '20px',
right: '20px',
cursor: 'pointer',
background: theme.palette.background.default,
width: "50px",
height: "50px",
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "50%",
width: '50px',
height: '50px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '50%',
}));
export const CodecTypography = styled(Typography)(({ theme }) => ({
fontSize: "18px",
fontSize: '18px',
}));
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
position: 'absolute',
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
borderRadius: '4px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '75%',
maxWidth: '900px',
padding: '15px 35px',
display: 'flex',
flexDirection: 'column',
gap: '17px',
overflowY: 'auto',
maxHeight: '95vh',
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
theme.palette.mode === 'dark'
? '0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)'
: 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px',
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
'&::-webkit-scrollbar-track:hover': {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
'&::-webkit-scrollbar': {
width: '16px',
height: '10px',
backgroundColor: theme.palette.mode === 'light' ? '#f6f8fa' : '#292d3e',
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.mode === 'light' ? '#d3d9e1' : '#575757',
borderRadius: '8px',
backgroundClip: 'content-box',
border: '4px solid transparent',
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme.palette.mode === 'light' ? '#b7bcc4' : '#474646',
},
}));
export const NewCrowdfundTitle = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "25px",
userSelect: "none",
fontSize: '25px',
userSelect: 'none',
}));
export const NewCrowdFundFont = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "18px",
userSelect: "none",
fontSize: '18px',
userSelect: 'none',
}));
export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
fontWeight: 400,
fontFamily: "Raleway",
fontSize: "18px",
userSelect: "none",
fontStyle: "italic",
textDecoration: "underline",
fontSize: '18px',
userSelect: 'none',
fontStyle: 'italic',
textDecoration: 'underline',
}));
export const CustomInputField = styled(TextField)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
borderColor: theme.palette.background.paper,
"& label": {
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& label': {
color: theme.palette.mode === 'light' ? '#808183' : '#edeef0',
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
},
"& label.Mui-focused": {
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
'& label.Mui-focused': {
color: theme.palette.mode === 'light' ? '#A0AAB4' : '#d7d8da',
},
"& .MuiInput-underline:after": {
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
'& .MuiInput-underline:after': {
borderBottomColor: theme.palette.mode === 'light' ? '#B2BAC2' : '#c9cccf',
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#E0E3E7',
},
"&:hover fieldset": {
borderColor: "#B2BAC2",
'&:hover fieldset': {
borderColor: '#B2BAC2',
},
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
'&.Mui-focused fieldset': {
borderColor: '#6F7E8C',
},
},
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiInputBase-root': {
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
},
"& [class$='-MuiFilledInput-root']": {
padding: "30px 12px 8px",
padding: '30px 12px 8px',
},
"& .MuiFilledInput-root:after": {
'& .MuiFilledInput-root:after': {
borderBottomColor: theme.palette.secondary.main,
},
}));
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
letterSpacing: '1px',
fontWeight: 400,
fontSize: "20px",
fontSize: '20px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
}));
export const CrowdfundSubTitleRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "start",
flexDirection: "row",
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
flexDirection: 'row',
});
export const CrowdfundSubTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
letterSpacing: '1px',
fontWeight: 400,
fontSize: "17px",
fontSize: '17px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
borderBottom: `1px solid ${theme.palette.text.primary}`,
paddingBottom: "1.5px",
width: "fit-content",
textDecoration: "none",
paddingBottom: '1.5px',
width: 'fit-content',
textDecoration: 'none',
}));
export const CrowdfundDescription = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "16px",
fontSize: '16px',
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
userSelect: 'none',
wordBreak: 'break-word',
}));
export const Spacer = ({ height }: any) => {
@@ -212,326 +203,313 @@ export const Spacer = ({ height }: any) => {
};
export const StyledCardHeaderComment = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "5px",
padding: "7px 0px",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '5px',
padding: '7px 0px',
});
export const StyledCardCol = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const StyledCardColComment = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const AuthorTextComment = styled(Typography)({
fontFamily: "Raleway, sans-serif",
fontSize: "16px",
lineHeight: "1.2",
fontSize: '16px',
lineHeight: '1.2',
});
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
color: "#fff",
height: "25px",
width: "auto",
color: '#fff',
height: '25px',
width: 'auto',
}));
export const CoverImagePreview = styled("img")(({ theme }) => ({
width: "100px",
height: "100px",
objectFit: "contain",
userSelect: "none",
borderRadius: "3px",
marginBottom: "10px",
export const CoverImagePreview = styled('img')(({ theme }) => ({
width: '100px',
height: '100px',
objectFit: 'contain',
userSelect: 'none',
borderRadius: '3px',
marginBottom: '10px',
}));
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "10px",
display: 'flex',
alignItems: 'center',
gap: '10px',
}));
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: "50%",
padding: "5px",
transition: "all 0.2s ease-in-out",
"&:hover": {
cursor: "pointer",
scale: "1.1",
borderRadius: '50%',
padding: '5px',
transition: 'all 0.2s ease-in-out',
'&:hover': {
cursor: 'pointer',
scale: '1.1',
},
}));
export const CrowdfundCardTitle = styled(DoubleLine)(({ theme }) => ({
fontFamily: "Montserrat",
fontSize: "24px",
letterSpacing: "-0.3px",
userSelect: "none",
marginBottom: "auto",
textAlign: "center",
"@media (max-width: 650px)": {
fontSize: "18px",
fontSize: '24px',
letterSpacing: '-0.3px',
userSelect: 'none',
marginBottom: 'auto',
textAlign: 'center',
'@media (max-width: 650px)': {
fontSize: '18px',
},
}));
export const CrowdfundUploadDate = styled(Typography)(({ theme }) => ({
fontFamily: "Montserrat",
fontSize: "12px",
letterSpacing: "0.2px",
fontSize: '12px',
letterSpacing: '0.2px',
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CATContainer = styled(Box)(({ theme }) => ({
position: "relative",
display: "flex",
padding: "15px",
flexDirection: "column",
gap: "20px",
justifyContent: "center",
width: "100%",
alignItems: "center",
position: 'relative',
display: 'flex',
padding: '15px',
flexDirection: 'column',
gap: '20px',
justifyContent: 'center',
width: '100%',
alignItems: 'center',
}));
export const AddCrowdFundButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
textTransform: "none",
padding: "10px 25px",
fontSize: "15px",
gap: "8px",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
textTransform: 'none',
padding: '10px 25px',
fontSize: '15px',
gap: '8px',
color: '#ffffff',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
border: "none",
borderRadius: "5px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
theme.palette.mode === 'dark' ? theme.palette.primary.main : '#2a9a86',
border: 'none',
borderRadius: '5px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
theme.palette.mode === 'dark' ? theme.palette.primary.dark : '#217e6d',
},
}));
export const EditCrowdFundButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
textTransform: "none",
padding: "5px 12px",
gap: "8px",
color: "#ffffff",
display: 'flex',
alignItems: 'center',
textTransform: 'none',
padding: '5px 12px',
gap: '8px',
color: '#ffffff',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
border: "none",
borderRadius: "5px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
theme.palette.mode === 'dark' ? theme.palette.primary.main : '#2a9a86',
border: 'none',
borderRadius: '5px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor:
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
theme.palette.mode === 'dark' ? theme.palette.primary.dark : '#217e6d',
},
}));
export const CrowdfundListWrapper = styled(Box)(({ theme }) => ({
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "0px",
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: '0px',
background: theme.palette.background.default,
}));
export const CrowdfundTitleRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
gap: "10px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '10px',
}));
export const CrowdfundPageTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
fontSize: "35px",
fontSize: '35px',
fontWeight: 400,
letterSpacing: "1px",
userSelect: "none",
letterSpacing: '1px',
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundStatusRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "Mulish",
fontSize: "21px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '21px',
fontWeight: 400,
letterSpacing: 0,
border: `1px solid ${theme.palette.text.primary}`,
borderRadius: "8px",
padding: "15px 25px",
userSelect: "none",
borderRadius: '8px',
padding: '15px 25px',
userSelect: 'none',
}));
export const CrowdfundDescriptionRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontFamily: "Montserrat",
fontSize: "18px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
fontSize: '18px',
fontWeight: 400,
letterSpacing: 0,
});
export const AboutMyCrowdfund = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
fontSize: "23px",
fontSize: '23px',
fontWeight: 400,
letterSpacing: "1px",
userSelect: "none",
letterSpacing: '1px',
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundInlineContentRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
});
export const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
display: "flex",
fontFamily: "Mulish",
fontSize: "19px",
display: 'flex',
fontSize: '19px',
fontWeight: 400,
letterSpacing: 0,
userSelect: "none",
userSelect: 'none',
color: theme.palette.text.primary,
}));
export const CrowdfundAccordion = styled(Accordion)(({ theme }) => ({
backgroundColor: theme.palette.primary.light,
"& .Mui-expanded": {
minHeight: "auto !important",
'& .Mui-expanded': {
minHeight: 'auto !important',
},
}));
export const CrowdfundAccordionSummary = styled(AccordionSummary)({
height: "50px",
"& .Mui-expanded": {
margin: "0px !important",
height: '50px',
'& .Mui-expanded': {
margin: '0px !important',
},
});
export const CrowdfundAccordionFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "20px",
fontSize: '20px',
fontWeight: 400,
letterSpacing: "0px",
letterSpacing: '0px',
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CrowdfundAccordionDetails = styled(AccordionDetails)({
padding: "0px 16px 16px 16px",
padding: '0px 16px 16px 16px',
});
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "16px",
display: 'flex',
alignItems: 'center',
fontSize: '16px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
}));
export const CoverImage = styled("img")({
width: "100%",
height: "250px",
objectFit: "cover",
objectPosition: "center",
export const CoverImage = styled('img')({
width: '100%',
height: '250px',
objectFit: 'cover',
objectPosition: 'center',
});
export const CrowdfundActionButtonRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
});
export const CrowdfundActionButton = styled(Button)(({ theme }) => ({
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "16px",
display: 'flex',
alignItems: 'center',
fontSize: '16px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
}));
export const BackToHomeButton = styled(Button)(({ theme }) => ({
position: "absolute",
top: "20px",
left: "20px",
display: "flex",
alignItems: "center",
fontFamily: "Montserrat",
fontSize: "13px",
position: 'absolute',
top: '20px',
left: '20px',
display: 'flex',
alignItems: 'center',
fontSize: '13px',
fontWeight: 400,
letterSpacing: "0.2px",
color: "white",
gap: "5px",
padding: "5px 10px",
letterSpacing: '0.2px',
color: 'white',
gap: '5px',
padding: '5px 10px',
backgroundColor: theme.palette.secondary.main,
transition: "all 0.3s ease-in-out",
"&:hover": {
transition: 'all 0.3s ease-in-out',
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
cursor: "pointer",
cursor: 'pointer',
},
}));
export const CrowdfundLoaderRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "10px",
padding: "10px",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
padding: '10px',
});
export const RatingContainer = styled(Box)({
display: "flex",
alignItems: "center",
padding: "1px 5px",
borderRadius: "5px",
backgroundColor: "transparent",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
backgroundColor: "#e4ddddac",
display: 'flex',
alignItems: 'center',
padding: '1px 5px',
borderRadius: '5px',
backgroundColor: 'transparent',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
backgroundColor: '#e4ddddac',
},
});
export const StyledRating = styled(Rating)({
fontSize: "28px",
fontSize: '28px',
});
export const NoReviewsFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
fontWeight: 400,
letterSpacing: 0,
color: theme.palette.text.primary,
@@ -543,43 +521,40 @@ export const StyledButton = styled(Button)(({ theme }) => ({
}));
export const CustomSelect = styled(Select)(({ theme }) => ({
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
"& .MuiSelect-select": {
padding: "12px",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiSelect-select': {
padding: '12px',
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
borderRadius: theme.shape.borderRadius, // Match border radius
},
"&:before": {
'&:before': {
// Underline style
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
borderBottomColor: theme.palette.mode === 'light' ? '#B2BAC2' : '#c9cccf',
},
"&:after": {
'&:after': {
// Underline style when focused
borderBottomColor: theme.palette.secondary.main,
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#E0E3E7',
},
"&:hover fieldset": {
borderColor: "#B2BAC2",
'&:hover fieldset': {
borderColor: '#B2BAC2',
},
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
'&.Mui-focused fieldset': {
borderColor: '#6F7E8C',
},
},
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
'& .MuiInputBase-root': {
fontSize: '19px',
letterSpacing: '0px',
fontWeight: 400,
color: theme.palette.text.primary,
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect, CSSProperties } from "react";
import Skeleton from "@mui/material/Skeleton";
import { Box } from "@mui/material";
import DeletedVideo from "../assets/img/DeletedVideo.jpg";
import React, { useState, useEffect, CSSProperties } from 'react';
import Skeleton from '@mui/material/Skeleton';
import { Box } from '@mui/material';
import DeletedVideo from '../assets/img/DeletedVideo.jpg';
import { useIsMobile } from '../hooks/useIsMobile';
interface ResponsiveImageProps {
src: string;
width: number;
@@ -9,6 +10,7 @@ interface ResponsiveImageProps {
alt?: string;
className?: string;
style?: CSSProperties;
fill?: boolean;
}
const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
@@ -18,7 +20,10 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
alt,
className,
style,
fill,
}) => {
const isMobile = useIsMobile();
const [loading, setLoading] = useState(true);
const endLoading = (endTimeSeconds: number) => {
@@ -33,34 +38,38 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
return (
<Box
sx={{
padding: "2px",
height: "100%",
padding: isMobile ? '0px' : '5px',
height: '100%',
backgroundColor: '#050507',
...style,
}}
boxShadow={2}
>
{loading && (
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: 0,
paddingBottom: `${(height / width) * 100}%`,
objectFit: "contain",
visibility: loading ? "visible" : "hidden",
borderRadius: "8px",
width: '100%',
height: '100%',
objectFit: 'contain',
visibility: loading ? 'visible' : 'hidden',
borderRadius: '8px',
}}
/>
)}
<img
onLoad={() => setLoading(false)}
src={!src && !loading ? DeletedVideo : src || null}
src={!src && !loading ? DeletedVideo : src || undefined}
style={{
width: "100%",
height: "100%",
borderRadius: "8px",
display: loading ? "none" : "unset",
objectFit: "contain",
width: '100%',
height: '100%',
borderRadius: isMobile ? '0px' : '8px',
display: loading ? 'none' : 'unset',
objectFit: fill ? 'fill' : 'contain',
maskImage: 'radial-gradient(circle, black 95%, transparent 100%)',
maskMode: 'alpha',
}}
/>
</Box>

View File

@@ -1,62 +1,65 @@
import React, { useEffect } from "react";
import { styled } from "@mui/system";
import { Grid } from "@mui/material";
import { useFetchVideos } from "../hooks/useFetchVideos.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../state/store.ts";
import { signal } from "@preact/signals-react";
import { useEffect } from 'react';
import { styled } from '@mui/system';
import { Grid } from '@mui/material';
import { useFetchVideos } from '../hooks/useFetchVideos.tsx';
import { useAtom } from 'jotai';
import {
totalNamesPublishedAtom,
totalVideosPublishedAtom,
videosPerNamePublishedAtom,
} from '../state/global/stats.ts';
/* eslint-disable react-refresh/only-export-components */
export const totalVideosPublished = signal(0);
export const totalNamesPublished = signal(0);
export const videosPerNamePublished = signal(0);
export const StatsData = () => {
const [totalVideosPublished] = useAtom(totalVideosPublishedAtom);
const [totalNamesPublished] = useAtom(totalNamesPublishedAtom);
const [videosPerNamePublished] = useAtom(videosPerNamePublishedAtom);
const StatsCol = styled(Grid)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "100%",
padding: "20px 0px",
display: 'flex',
flexDirection: 'column',
width: '100%',
padding: '20px 0px',
backgroundColor: theme.palette.background.default,
}));
const { getVideosCount } = useFetchVideos();
const showValueIfExists = (value: number) => {
return value > 0 ? "inline" : "none";
return value > 0 ? 'inline' : 'none';
};
const showStats = useSelector((state: RootState) => state.persist.showStats);
const showVideoCount = showValueIfExists(totalVideosPublished.value);
const showPublisherCount = showValueIfExists(totalNamesPublished.value);
const showAverage = showValueIfExists(videosPerNamePublished.value);
const showVideoCount = showValueIfExists(totalVideosPublished);
const showPublisherCount = showValueIfExists(totalNamesPublished);
const showAverage = showValueIfExists(videosPerNamePublished);
useEffect(() => {
getVideosCount();
}, [getVideosCount]);
return (
<StatsCol sx={{ display: showStats ? "block" : "none" }}>
<StatsCol sx={{ display: 'block' }}>
<div>
Videos:{" "}
<span style={{ fontWeight: "bold", display: showVideoCount }}>
{totalVideosPublished.value}
Videos:{' '}
<span style={{ fontWeight: 'bold', display: showVideoCount }}>
{totalVideosPublished}
</span>
</div>
<div>
Publishers:{" "}
<span style={{ fontWeight: "bold", display: showPublisherCount }}>
{totalNamesPublished.value}
Publishers:{' '}
<span style={{ fontWeight: 'bold', display: showPublisherCount }}>
{totalNamesPublished}
</span>
</div>
<div>
Average:{" "}
Average:{' '}
<span
style={{
fontWeight: "bold",
fontWeight: 'bold',
display: showAverage,
}}
>
{Number(videosPerNamePublished.value).toFixed(0)}
{Number(videosPerNamePublished).toFixed(0)}
</span>
</div>
</StatsCol>

View File

@@ -1,15 +1,11 @@
import { styled } from '@mui/system';
import {
Box,
Modal,
Typography
} from '@mui/material';
import { Box, Modal, Typography } from '@mui/material';
export const StyledModal = styled(Modal)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}))
justifyContent: 'center',
}));
export const ModalContent = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
@@ -17,12 +13,11 @@ export const ModalContent = styled(Box)(({ theme }) => ({
borderRadius: theme.spacing(1),
width: '40%',
'&:focus': {
outline: 'none'
}
}))
outline: 'none',
},
}));
export const ModalText = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "25px",
fontSize: '25px',
color: theme.palette.text.primary,
}));
}));

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState } from 'react';
import {
Box,
Button,
@@ -7,13 +7,14 @@ import {
SelectChangeEvent,
ListItem,
List,
useTheme
} from "@mui/material";
useTheme,
} from '@mui/material';
import {
StyledModal,
ModalContent,
ModalText
} from "./BlockedNamesModal-styles";
ModalText,
} from './BlockedNamesModal-styles';
import { useBlockedNames } from 'qapp-core';
interface PostModalProps {
open: boolean;
@@ -22,16 +23,17 @@ interface PostModalProps {
export const BlockedNamesModal: React.FC<PostModalProps> = ({
open,
onClose
onClose,
}) => {
const { removeFromBlockedList } = useBlockedNames();
const [blockedNames, setBlockedNames] = useState<string[]>([]);
const theme = useTheme();
const getBlockedNames = React.useCallback(async () => {
try {
const listName = `blockedNames`;
const response = await qortalRequest({
action: "GET_LIST_ITEMS",
list_name: listName
action: 'GET_LIST_ITEMS',
list_name: listName,
});
setBlockedNames(response);
} catch (error) {
@@ -45,11 +47,7 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
const removeFromBlockList = async (name: string) => {
try {
const response = await qortalRequest({
action: "DELETE_LIST_ITEM",
list_name: "blockedNames",
item: name
});
const response = await removeFromBlockedList([name]);
if (response === true) {
setBlockedNames((prev) => prev.filter((n) => n !== name));
@@ -63,18 +61,18 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
<ModalText>Manage blocked names</ModalText>
<List
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
flex: "1",
overflow: "auto"
width: '100%',
display: 'flex',
flexDirection: 'column',
flex: '1',
overflow: 'auto',
}}
>
{blockedNames.map((name, index) => (
<ListItem
key={name + index}
sx={{
display: "flex"
display: 'flex',
}}
>
<Typography>{name}</Typography>
@@ -82,7 +80,6 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
sx={{
backgroundColor: theme.palette.primary.light,
color: theme.palette.text.primary,
fontFamily: "Raleway"
}}
onClick={() => removeFromBlockList(name)}
>

View File

@@ -2,33 +2,41 @@ import {
Avatar,
Box,
Button,
ButtonBase,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
useTheme,
} from "@mui/material";
import React, { useCallback, useState, useEffect } from "react";
import { CommentEditor } from "./CommentEditor";
} from '@mui/material';
import { useCallback, useState } from 'react';
import { CommentEditor } from './CommentEditor';
import {
CardContentContainerComment,
CommentActionButtonRow,
CommentDateText,
CreatedTextComment,
EditReplyButton,
StyledCardComment,
} from "./Comments-styles";
import { StyledCardHeaderComment } from "./Comments-styles";
import { StyledCardColComment } from "./Comments-styles";
import { AuthorTextComment } from "./Comments-styles";
} from './Comments-styles';
import { StyledCardHeaderComment } from './Comments-styles';
import { StyledCardColComment } from './Comments-styles';
import { AuthorTextComment } from './Comments-styles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import {
StyledCardContentComment,
LoadMoreCommentsButton as CommentActionButton,
} from "./Comments-styles";
import { useSelector } from "react-redux";
import { RootState } from "../../../state/store";
import Portal from "../Portal";
import { formatDate } from "../../../utils/time";
} from './Comments-styles';
import Portal from '../Portal';
import { formatDate } from '../../../utils/time';
import { createAvatarLink, Spacer, useAuth } from 'qapp-core';
import { useIsSmall } from '../../../hooks/useIsSmall';
import { useTranslation } from 'react-i18next';
import i18n from '../../../i18n/i18n';
interface CommentProps {
comment: any;
postId: string;
@@ -41,12 +49,15 @@ export const Comment = ({
postName,
onSubmit,
}: CommentProps) => {
const { t } = useTranslation(['core']);
const isSmall = useIsSmall();
const [isReplying, setIsReplying] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
const { user } = useSelector((state: RootState) => state.auth);
const { name } = useAuth();
const [currentEdit, setCurrentEdit] = useState<any>(null);
const theme = useTheme();
const [isOpenReplies, setIsOpenReplies] = useState(false);
const handleSubmit = useCallback((comment: any, isEdit?: boolean) => {
onSubmit(comment, isEdit);
setCurrentEdit(null);
@@ -57,9 +68,9 @@ export const Comment = ({
<Box
id={comment?.identifier}
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
display: 'flex',
width: '100%',
flexDirection: 'column',
}}
>
{currentEdit && (
@@ -74,13 +85,13 @@ export const Comment = ({
<DialogContent>
<Box
sx={{
width: "300px",
display: "flex",
justifyContent: "center",
width: '300px',
display: 'flex',
justifyContent: 'center',
}}
>
<CommentEditor
onSubmit={obj => handleSubmit(obj, true)}
onSubmit={(obj) => handleSubmit(obj, true)}
postId={postId}
postName={postName}
isEdit
@@ -91,57 +102,76 @@ export const Comment = ({
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => setCurrentEdit(null)}>
Close
{t('core:action.close', {
postProcess: 'capitalizeFirstChar',
})}
</Button>
</DialogActions>
</Dialog>
</Portal>
)}
<CommentCard
name={comment?.name}
message={comment?.message}
replies={comment?.replies || []}
setCurrentEdit={setCurrentEdit}
created={comment?.created}
isOpenReplies={isOpenReplies}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
marginTop: "20px",
justifyContent: "space-between",
display: 'flex',
width: '100%',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
{comment?.created && (
<Typography
variant="h6"
sx={{
fontSize: "12px",
marginLeft: "5px",
{isReplying && (
<CommentEditor
onSubmit={handleSubmit}
postId={postId}
postName={postName}
isReply
commentId={comment.identifier}
onCloseReply={() => {
setIsReplying(false);
setIsEditing(false);
}}
color={theme.palette.text.primary}
>
{formatDate(+comment?.created)}
</Typography>
/>
)}
<CommentActionButtonRow>
<CommentActionButton
size="small"
variant="contained"
onClick={() => setIsReplying(true)}
>
reply
</CommentActionButton>
{user?.name === comment?.name && (
<CommentActionButton
size="small"
variant="contained"
onClick={() => setCurrentEdit(comment)}
>
edit
</CommentActionButton>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '5px',
marginTop: '20px',
justifyContent: 'space-between',
}}
>
<CommentActionButtonRow
sx={{
gap: '20px',
}}
>
{!isReplying && (
<ButtonBase onClick={() => setIsReplying(true)}>
<Typography>
{' '}
{t('core:action.reply', {
postProcess: 'capitalizeFirstChar',
})}
</Typography>
</ButtonBase>
)}
{isReplying && (
{/* {name === comment?.name && (
<ButtonBase onClick={() => setCurrentEdit(comment)}>
<Typography>Edit</Typography>
</ButtonBase>
)} */}
{/* {isReplying && (
<CommentActionButton
size="small"
variant="contained"
@@ -152,29 +182,42 @@ export const Comment = ({
>
close
</CommentActionButton>
)} */}
{comment?.replies && comment?.replies?.length > 0 && (
<ButtonBase onClick={() => setIsOpenReplies((prev) => !prev)}>
{isOpenReplies ? (
<ExpandLessIcon
sx={{
color: 'primary.dark',
}}
/>
) : (
<ExpandMoreIcon
sx={{
color: 'primary.dark',
}}
/>
)}
<Typography
color="primary.dark"
sx={{
fontSize: isSmall ? '14px' : 'unset',
}}
>
{isOpenReplies
? ` ${t('core:comments.hide_replies', {
postProcess: 'capitalizeFirstChar',
})} (${comment?.replies?.length})`
: ` ${t('core:comments.view_replies', {
postProcess: 'capitalizeFirstChar',
})} (${comment?.replies?.length})`}
</Typography>
</ButtonBase>
)}
</CommentActionButtonRow>
</Box>
</CommentCard>
<Box
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
alignItems: "center",
}}
>
{isReplying && (
<CommentEditor
onSubmit={handleSubmit}
postId={postId}
postName={postName}
isReply
commentId={comment.identifier}
/>
)}
</Box>
</Box>
);
};
@@ -186,37 +229,21 @@ export const CommentCard = ({
replies,
children,
setCurrentEdit,
isOpenReplies,
isReply,
}: any) => {
const [avatarUrl, setAvatarUrl] = React.useState<string>("");
const { user } = useSelector((state: RootState) => state.auth);
const { i18n } = useTranslation(['core']);
const theme = useTheme();
const getAvatar = React.useCallback(async (author: string) => {
try {
const url = await qortalRequest({
action: "GET_QDN_RESOURCE_URL",
name: author,
service: "THUMBNAIL",
identifier: "qortal_avatar",
});
setAvatarUrl(url);
} catch (error) {
console.error(error);
}
}, []);
useEffect(() => {
getAvatar(name);
}, [name]);
const isSmall = useIsSmall();
const { name: username } = useAuth();
const avatarUrl = createAvatarLink(name);
return (
<CardContentContainerComment>
<StyledCardHeaderComment
sx={{
"& .MuiCardHeader-content": {
overflow: "hidden",
'& .MuiCardHeader-content': {
overflow: 'hidden',
},
}}
>
@@ -224,72 +251,81 @@ export const CommentCard = ({
<Avatar
src={avatarUrl}
alt={`${name}'s avatar`}
sx={{ width: "35px", height: "35px" }}
sx={{
width: isReply && !isSmall ? '30px' : '40px',
height: isReply && !isSmall ? '30px' : '40px',
marginRight: '5px',
}}
/>
</Box>
<StyledCardColComment>
<AuthorTextComment>{name}</AuthorTextComment>
</StyledCardColComment>
<Box
sx={{
width: '100%',
}}
>
<StyledCardColComment
sx={{
flexDirection: 'row',
gap: '10px',
}}
>
<AuthorTextComment>{name}</AuthorTextComment>
<CreatedTextComment>
{formatDate(+created, i18n.language)}
</CreatedTextComment>
</StyledCardColComment>
<Spacer height="10px" />
<StyledCardContentComment>
<StyledCardComment>{message}</StyledCardComment>
</StyledCardContentComment>
{children}
</Box>
</StyledCardHeaderComment>
<StyledCardContentComment>
<StyledCardComment>{message}</StyledCardComment>
</StyledCardContentComment>
<Box
sx={{
paddingLeft: "15px",
display: "flex",
flexDirection: "column",
}}
>
{replies?.map((reply: any) => {
return (
<Box
key={reply?.identifier}
id={reply?.identifier}
sx={{
display: "flex",
border: "1px solid grey",
borderRadius: "10px",
marginTop: "8px",
}}
>
<CommentCard
name={reply?.name}
message={reply?.message}
setCurrentEdit={setCurrentEdit}
<Collapse in={isOpenReplies} timeout="auto" unmountOnExit>
<Box
sx={{
paddingLeft: isSmall ? '2px' : '50px',
paddingTop: '10px',
display: 'flex',
flexDirection: 'column',
width: '100%',
border: isSmall ? '1px solid grey' : 'unset',
paddingRight: isSmall ? '2px' : 'unset',
}}
>
{replies?.map((reply: any) => {
return (
<Box
key={reply?.identifier}
id={reply?.identifier}
sx={{
display: 'flex',
borderRadius: '10px',
marginTop: '8px',
width: '100%',
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
}}
<CommentCard
name={reply?.name}
message={reply?.message}
setCurrentEdit={setCurrentEdit}
created={reply?.created}
isReply
>
{reply?.created && (
<CommentDateText>
{formatDate(+reply?.created)}
</CommentDateText>
)}
{user?.name === reply?.name ? (
<EditReplyButton
size="small"
variant="contained"
onClick={() => setCurrentEdit(reply)}
sx={{}}
>
edit
</EditReplyButton>
) : (
<Box />
)}
</Box>
</CommentCard>
</Box>
);
})}
</Box>
{children}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '5px',
justifyContent: 'space-between',
}}
></Box>
</CommentCard>
</Box>
);
})}
</Box>
</Collapse>
</CardContentContainerComment>
);
};

View File

@@ -1,26 +1,27 @@
import { Box, Button, TextField } from "@mui/material";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../state/store";
import ShortUniqueId from "short-unique-id";
import { setNotification } from "../../../state/features/notificationsSlice";
import {hashWordWithoutPublicSalt} from 'qapp-core'
import {
publishFormatter,
stringToFile,
} from "../../../utils/PublishFormatter.ts";
import localforage from "localforage";
import { useEffect, useState } from 'react';
import ShortUniqueId from 'short-unique-id';
import { hashWordWithoutPublicSalt, useAuth } from 'qapp-core';
import localforage from 'localforage';
import {
CommentInput,
CommentInputContainer,
SubmitCommentButton,
} from "./Comments-styles";
} from './Comments-styles';
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
import { COMMENT_BASE } from '../../../constants/Identifiers.ts';
import { useSetAtom } from 'jotai';
import {
AltertObject,
setNotificationAtom,
} from '../../../state/global/notifications.ts';
import { Box, Button } from '@mui/material';
import { useIsSmall } from '../../../hooks/useIsSmall.tsx';
import { useTranslation } from 'react-i18next';
const uid = new ShortUniqueId({ length: 7 });
const notification = localforage.createInstance({
name: "notification",
name: 'notification',
});
const MAX_ITEMS = 10;
@@ -35,11 +36,11 @@ export interface Item {
export async function addItem(item: Item): Promise<void> {
// Get all items
const notificationComments: Item[] =
(await notification.getItem("comments")) || [];
(await notification.getItem('comments')) || [];
// Find the item with the same id, if it exists
const existingItemIndex = notificationComments.findIndex(
i => i.id === item.id
(i) => i.id === item.id
);
if (existingItemIndex !== -1) {
@@ -57,15 +58,15 @@ export async function addItem(item: Item): Promise<void> {
}
// Store the items back into localForage
await notification.setItem("comments", notificationComments);
await notification.setItem('comments', notificationComments);
}
export async function updateItemDate(item: any): Promise<void> {
// Get all items
const notificationComments: Item[] =
(await notification.getItem("comments")) || [];
(await notification.getItem('comments')) || [];
const notificationCreatorComment: any =
(await notification.getItem("post-comments")) || {};
(await notification.getItem('post-comments')) || {};
const findPostId = notificationCreatorComment[item.postId];
if (findPostId) {
notificationCreatorComment[item.postId].lastSeen = item.lastSeen;
@@ -79,8 +80,8 @@ export async function updateItemDate(item: any): Promise<void> {
});
// Store the items back into localForage
await notification.setItem("comments", notificationComments);
await notification.setItem("post-comments", notificationCreatorComment);
await notification.setItem('comments', notificationComments);
await notification.setItem('post-comments', notificationCreatorComment);
}
interface CommentEditorProps {
postId: string;
@@ -90,13 +91,14 @@ interface CommentEditorProps {
commentId?: string;
isEdit?: boolean;
commentMessage?: string;
onCloseReply?: () => void;
}
function utf8ToBase64(inputString: string): string {
// Encode the string as UTF-8
const utf8String = encodeURIComponent(inputString).replace(
/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(Number("0x" + p1))
(match, p1) => String.fromCharCode(Number('0x' + p1))
);
// Convert the UTF-8 encoded string to base64
@@ -112,11 +114,15 @@ export const CommentEditor = ({
commentId,
isEdit,
commentMessage,
onCloseReply,
}: CommentEditorProps) => {
const [value, setValue] = useState<string>("");
const dispatch = useDispatch();
const { user } = useSelector((state: RootState) => state.auth);
const { t } = useTranslation(['core']);
const isSmall = useIsSmall();
const [value, setValue] = useState<string>('');
const { name, address } = useAuth();
const setNotification = useSetAtom(setNotificationAtom);
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
if (isEdit && commentMessage) {
setValue(commentMessage);
@@ -127,45 +133,43 @@ export const CommentEditor = ({
identifier: string,
idForNotification?: string
) => {
const address = user?.address;
const name = user?.name || "";
let errorMsg = "";
let errorMsg = '';
if (!address) {
errorMsg = "Cannot post: your address isn't available";
}
if (!name) {
errorMsg = "Cannot post without a name";
errorMsg = 'Cannot post without a name';
}
if (value.length > 200) {
errorMsg = "Comment needs to be under 200 characters";
errorMsg = 'Comment needs to be under 200 characters';
}
if (errorMsg) {
dispatch(
setNotification({
msg: errorMsg,
alertType: "error",
})
);
const notificationObj: AltertObject = {
msg: errorMsg,
alertType: 'error',
};
setNotification(notificationObj);
throw new Error(errorMsg);
}
try {
const resourceResponse = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "BLOG_COMMENT",
action: 'PUBLISH_QDN_RESOURCE',
name: name!,
service: 'BLOG_COMMENT',
data64: utf8ToBase64(value),
identifier: identifier,
});
dispatch(
setNotification({
msg: "Comment successfully published",
alertType: "success",
})
);
const notificationObj: AltertObject = {
msg: 'Comment successfully published',
alertType: 'success',
};
setNotification(notificationObj);
if (idForNotification) {
addItem({
id: idForNotification,
@@ -176,40 +180,27 @@ export const CommentEditor = ({
}
return resourceResponse;
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {
notificationObj = {
msg: error || "Failed to publish comment",
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || "Failed to publish comment",
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || "Failed to publish comment",
alertType: "error",
};
}
if (!notificationObj) throw new Error("Failed to publish comment");
dispatch(setNotification(notificationObj));
throw new Error("Failed to publish comment");
} catch (error) {
const isError = error instanceof Error;
const message = isError ? error?.message : 'Failed to publish comment';
const notificationObj: AltertObject = {
msg: message,
alertType: 'error',
};
setNotification(notificationObj);
throw new Error('Failed to publish comment');
}
};
const handleSubmit = async () => {
try {
const id = uid.rnd();
const hashPostId = await hashWordWithoutPublicSalt(postId, 20)
const hashPostId = await hashWordWithoutPublicSalt(postId, 20);
let identifier = `${COMMENT_BASE}${hashPostId}_base_${id}`;
let idForNotification = identifier;
if (isReply && commentId) {
const removeBaseCommentId = commentId;
removeBaseCommentId.replace("_base_", "");
removeBaseCommentId.replace('_base_', '');
identifier = `${COMMENT_BASE}${hashPostId}_reply_${removeBaseCommentId.slice(-6)}_${id}`;
idForNotification = commentId;
}
@@ -222,20 +213,28 @@ export const CommentEditor = ({
created: Date.now(),
identifier,
message: value,
service: "BLOG_COMMENT",
name: user?.name,
service: 'BLOG_COMMENT',
name: name,
});
setValue("");
setValue('');
} catch (error) {
console.error(error);
}
};
return (
<CommentInputContainer>
<CommentInputContainer
sx={{
width: isSmall ? '100%' : '90%',
}}
>
<CommentInput
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
id="standard-multiline-flexible"
label="Your comment"
label={t('core:comments.your_comment', {
postProcess: 'capitalizeFirstChar',
})}
multiline
maxRows={4}
variant="filled"
@@ -243,13 +242,53 @@ export const CommentEditor = ({
inputProps={{
maxLength: 200,
}}
InputLabelProps={{ style: { fontSize: "18px" } }}
onChange={e => setValue(e.target.value)}
InputLabelProps={{ style: { fontSize: '18px' } }}
onChange={(e) => setValue(e.target.value)}
/>
<SubmitCommentButton variant="contained" onClick={handleSubmit}>
{isReply ? "Submit reply" : isEdit ? "Edit" : "Submit comment"}
</SubmitCommentButton>
<Box
sx={{
width: '100%',
justifyContent: 'flex-end',
display: 'flex',
gap: '20px',
visibility: isReply
? 'visible'
: value || isFocused
? 'visible'
: 'hidden',
}}
>
<Button
onClick={(e) => {
setValue('');
if (!onCloseReply) return;
onCloseReply();
}}
variant="text"
>
{t('core:action.cancel', {
postProcess: 'capitalizeEachFirstChar',
})}
</Button>
<SubmitCommentButton
variant="contained"
color="info"
onClick={handleSubmit}
>
{isReply
? t('core:comments.submit_reply', {
postProcess: 'capitalizeFirstChar',
})
: isEdit
? t('core:action.edit', {
postProcess: 'capitalizeFirstChar',
})
: t('core:action.comment', {
postProcess: 'capitalizeFirstChar',
})}
</SubmitCommentButton>
</Box>
</CommentInputContainer>
);
};

View File

@@ -1,11 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CommentEditor } from "./CommentEditor";
import { Comment } from "./Comment";
import { Box, Button, CircularProgress, useTheme } from "@mui/material";
import { styled } from "@mui/system";
import { useSelector } from "react-redux";
import { RootState } from "../../../state/store";
import { useNavigate, useLocation } from "react-router-dom";
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CommentEditor } from './CommentEditor';
import { Comment } from './Comment';
import { Box, Button, CircularProgress, useTheme } from '@mui/material';
import { styled } from '@mui/system';
import { useNavigate, useLocation } from 'react-router-dom';
import {
CommentContainer,
CommentEditorContainer,
@@ -13,20 +11,21 @@ import {
LoadMoreCommentsButton,
LoadMoreCommentsButtonRow,
NoCommentsRow,
} from "./Comments-styles";
} from './Comments-styles';
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
import { hashWordWithoutPublicSalt } from "qapp-core";
} from '../../Publish/PublishVideo/PublishVideo-styles.tsx';
import { COMMENT_BASE } from '../../../constants/Identifiers.ts';
import { hashWordWithoutPublicSalt } from 'qapp-core';
import { useTranslation } from 'react-i18next';
interface CommentSectionProps {
postId: string;
postName: string;
}
const Panel = styled("div")`
const Panel = styled('div')`
display: flex;
flex-direction: column;
justify-content: start;
@@ -51,19 +50,19 @@ const Panel = styled("div")`
}
`;
export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
const { t } = useTranslation(['core']);
const navigate = useNavigate();
const location = useLocation();
const [listComments, setListComments] = useState<any[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false);
const { user } = useSelector((state: RootState) => state.auth);
const [newMessages, setNewMessages] = useState(0);
const [loadingComments, setLoadingComments] = useState<boolean>(false);
// console.log("postId is: ", postId, " postName is: ", postName);
const onSubmit = (obj?: any, isEdit?: boolean) => {
if (isEdit) {
setListComments((prev: any[]) => {
const findCommentIndex = prev.findIndex(
item => item?.identifier === obj?.identifier
(item) => item?.identifier === obj?.identifier
);
if (findCommentIndex === -1) return prev;
@@ -74,7 +73,7 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
return;
}
setListComments(prev => [
setListComments((prev) => [
...prev,
{
...obj,
@@ -84,9 +83,9 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
useEffect(() => {
const query = new URLSearchParams(location.search);
let commentVar = query?.get("comment");
let commentVar = query?.get('comment');
if (commentVar) {
if (commentVar && commentVar.endsWith("/")) {
if (commentVar && commentVar.endsWith('/')) {
commentVar = commentVar.slice(0, -1);
}
setIsOpen(true);
@@ -94,9 +93,9 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
const el = document.getElementById(commentVar);
if (el) {
el.scrollIntoView();
el.classList.add("glow");
el.classList.add('glow');
setTimeout(() => {
el.classList.remove("glow");
el.classList.remove('glow');
}, 2000);
}
navigate(location.pathname, { replace: true });
@@ -107,16 +106,16 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
const getReplies = useCallback(
async (commentId, postId) => {
const offset = 0;
const hashPostId = await hashWordWithoutPublicSalt(postId, 20)
const removeBaseCommentId = commentId.replace("_base_", "");
const hashPostId = await hashWordWithoutPublicSalt(postId, 20);
const removeBaseCommentId = commentId.replace('_base_', '');
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${COMMENT_BASE}${hashPostId}_reply_${removeBaseCommentId.slice(
-6
)}&limit=0&includemetadata=false&offset=${offset}&reverse=false&excludeblocked=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -126,9 +125,9 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
if (comment.identifier && comment.name) {
const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
@@ -154,26 +153,24 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
if (isNewMessages && numberOfComments) {
offset = numberOfComments;
}
const hashPostId = await hashWordWithoutPublicSalt(postId, 20)
const hashPostId = await hashWordWithoutPublicSalt(postId, 20);
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${COMMENT_BASE}${hashPostId}_base_&limit=20&includemetadata=false&offset=${offset}&reverse=false&excludeblocked=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
// console.log("url is: ", url);
// console.log("response is: ", responseData);
let comments: any[] = [];
for (const comment of responseData) {
if (comment.identifier && comment.name) {
const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
@@ -189,7 +186,7 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
}
}
if (isNewMessages) {
setListComments(prev => [...prev, ...comments]);
setListComments((prev) => [...prev, ...comments]);
setNewMessages(0);
} else {
setListComments(comments);
@@ -209,12 +206,12 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
const structuredCommentList = useMemo(() => {
return listComments.reduce((acc, curr, index, array) => {
if (curr?.identifier?.includes("_reply_")) {
if (curr?.identifier?.includes('_reply_')) {
return acc;
}
acc.push({
...curr,
replies: array.filter(comment =>
replies: array.filter((comment) =>
comment.identifier.includes(`_reply_${curr.identifier.slice(-6)}`)
),
});
@@ -225,6 +222,13 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
return (
<>
<Panel>
<CommentEditorContainer>
<CommentEditor
onSubmit={onSubmit}
postId={postId}
postName={postName}
/>
</CommentEditorContainer>
<CommentsContainer>
{loadingComments ? (
<NoCommentsRow>
@@ -254,25 +258,20 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
getComments(
true,
listComments.filter(
item => !item.identifier.includes("_reply_")
(item) => !item.identifier.includes('_reply_')
).length
);
}}
variant="contained"
size="small"
>
Load More Comments
{t('core:comments.load_more_comments', {
postProcess: 'capitalizeEachFirstChar',
})}
</LoadMoreCommentsButton>
</LoadMoreCommentsButtonRow>
)}
</CommentsContainer>
<CommentEditorContainer>
<CommentEditor
onSubmit={onSubmit}
postId={postId}
postName={postName}
/>
</CommentEditorContainer>
</Panel>
</>
);

View File

@@ -1,281 +1,277 @@
import { styled } from "@mui/system";
import { Card, Box, Typography, Button, TextField } from "@mui/material";
import { styled } from '@mui/system';
import { Card, Box, Typography, Button, TextField } from '@mui/material';
export const StyledCard = styled(Card)(({ theme }) => ({
backgroundColor:
theme.palette.mode === "light"
theme.palette.mode === 'light'
? theme.palette.primary.main
: theme.palette.primary.dark,
maxWidth: "600px",
width: "100%",
margin: "10px 0px",
cursor: "pointer",
"@media (max-width: 450px)": {
width: "100%;",
maxWidth: '600px',
width: '100%',
margin: '10px 0px',
cursor: 'pointer',
'@media (max-width: 450px)': {
width: '100%;',
},
}));
export const CardContentContainer = styled(Box)(({ theme }) => ({
backgroundColor:
theme.palette.mode === "light"
theme.palette.mode === 'light'
? theme.palette.primary.dark
: theme.palette.primary.light,
margin: "5px 10px",
borderRadius: "15px",
margin: '5px 10px',
borderRadius: '15px',
}));
export const CardContentContainerComment = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.mode === "light" ? "#a9d9d038" : "#c3abe414",
border: `1px solid ${theme.palette.primary.main}`,
margin: "0px",
padding: "8px 15px",
borderRadius: "8px",
width: "100%",
display: "flex",
flexDirection: "column",
// backgroundColor: theme.palette.mode === 'light' ? '#a9d9d038' : '#c3abe414',
// border: `1px solid ${theme.palette.primary.main}`,
margin: '0px',
// padding: '8px 15px',
borderRadius: '8px',
width: '100%',
display: 'flex',
flexDirection: 'column',
}));
export const StyledCardHeader = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "5px",
padding: "7px",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '5px',
padding: '7px',
});
export const StyledCardHeaderComment = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "7px",
padding: "9px 7px",
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-start',
gap: '7px',
padding: '0px 7px 9px 0px',
});
export const StyledCardCol = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const StyledCardColComment = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%',
});
export const StyledCardContent = styled(Box)({
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-start",
padding: "5px 10px",
gap: "10px",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '5px 10px',
gap: '10px',
});
export const StyledCardContentComment = styled(Box)({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "flex-start",
padding: "5px 10px",
gap: "10px",
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
// padding: '5px 10px',
gap: '10px',
});
export const StyledCardComment = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
letterSpacing: 0,
fontWeight: 400,
fontWeight: 300,
color: theme.palette.text.primary,
fontSize: "19px",
wordBreak: "break-word",
fontSize: '19px',
wordBreak: 'break-word',
}));
export const TitleText = styled(Typography)({
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
width: "100%",
fontFamily: "Cairo, sans-serif",
fontSize: "22px",
lineHeight: "1.2",
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
fontSize: '22px',
lineHeight: '1.2',
});
export const AuthorText = styled(Typography)({
fontFamily: "Raleway, sans-serif",
fontSize: "16px",
lineHeight: "1.2",
fontSize: '16px',
lineHeight: '1.2',
});
export const AuthorTextComment = styled(Typography)(({ theme }) => ({
fontFamily: "Montserrat, sans-serif",
fontSize: "17px",
letterSpacing: "0.3px",
fontWeight: 400,
fontSize: '17px',
letterSpacing: '0.3px',
fontWeight: 500,
color: theme.palette.text.primary,
userSelect: "none",
userSelect: 'none',
}));
export const CreatedTextComment = styled(Typography)(({ theme }) => ({
fontSize: '17px',
letterSpacing: '0.3px',
fontWeight: 300,
color: theme.palette.text.primary,
userSelect: 'none',
}));
export const IconsBox = styled(Box)({
display: "flex",
gap: "3px",
position: "absolute",
top: "12px",
right: "5px",
transition: "all 0.3s ease-in-out",
display: 'flex',
gap: '3px',
position: 'absolute',
top: '12px',
right: '5px',
transition: 'all 0.3s ease-in-out',
});
export const BookmarkIconContainer = styled(Box)({
display: "flex",
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
backgroundColor: "#fbfbfb",
color: "#50e3c2",
padding: "5px",
borderRadius: "3px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
transform: "scale(1.1)",
display: 'flex',
boxShadow: 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;',
backgroundColor: '#fbfbfb',
color: '#50e3c2',
padding: '5px',
borderRadius: '3px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
transform: 'scale(1.1)',
},
});
export const BlockIconContainer = styled(Box)({
display: "flex",
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
backgroundColor: "#fbfbfb",
color: "#c25252",
padding: "5px",
borderRadius: "3px",
transition: "all 0.3s ease-in-out",
"&:hover": {
cursor: "pointer",
transform: "scale(1.1)",
display: 'flex',
boxShadow: 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;',
backgroundColor: '#fbfbfb',
color: '#c25252',
padding: '5px',
borderRadius: '3px',
transition: 'all 0.3s ease-in-out',
'&:hover': {
cursor: 'pointer',
transform: 'scale(1.1)',
},
});
export const CommentsContainer = styled(Box)({
width: "90%",
maxWidth: "1000px",
display: "flex",
flexDirection: "column",
flex: "1",
overflow: "auto",
width: '100%',
maxWidth: '1000px',
display: 'flex',
flexDirection: 'column',
flex: '1',
overflow: 'auto',
});
export const CommentContainer = styled(Box)({
display: "flex",
flexDirection: "column",
margin: "25px 0px 50px 0px",
maxWidth: "100%",
width: "100%",
gap: "10px",
padding: "0px 5px",
display: 'flex',
flexDirection: 'column',
margin: '10px 0px 50px 0px',
maxWidth: '100%',
width: '100%',
gap: '10px',
padding: '0px 5px',
});
export const NoCommentsRow = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "start",
flex: "1",
padding: "10px 0px",
fontFamily: "Mulish",
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
flex: '1',
padding: '10px 0px',
letterSpacing: 0,
fontWeight: 400,
fontSize: "18px",
fontSize: '18px',
});
export const LoadMoreCommentsButtonRow = styled(Box)({
display: "flex",
display: 'flex',
});
export const EditReplyButton = styled(Button)(({ theme }) => ({
width: "30px",
alignSelf: "flex-end",
width: '30px',
alignSelf: 'flex-end',
background: theme.palette.primary.light,
color: "#ffffff",
color: '#ffffff',
}));
export const LoadMoreCommentsButton = styled(Button)(({ theme }) => ({
fontFamily: "Montserrat",
fontWeight: 400,
letterSpacing: "0.2px",
fontSize: "15px",
letterSpacing: '0.2px',
fontSize: '15px',
backgroundColor: theme.palette.primary.main,
color: "#ffffff",
color: '#ffffff',
}));
export const CommentActionButtonRow = styled(Box)({
display: "flex",
alignItems: "center",
gap: "5px",
display: 'flex',
alignItems: 'center',
gap: '5px',
});
export const CommentEditorContainer = styled(Box)({
width: "100%",
display: "flex",
justifyContent: "start",
width: '100%',
display: 'flex',
justifyContent: 'start',
});
export const CommentDateText = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
letterSpacing: 0,
fontWeight: 400,
fontSize: "13px",
marginLeft: "5px",
fontSize: '13px',
marginLeft: '5px',
color: theme.palette.text.primary,
}));
export const CommentInputContainer = styled(Box)({
display: "flex",
flexDirection: "column",
marginTop: "15px",
width: "90%",
maxWidth: "1000px",
borderRadius: "8px",
gap: "10px",
alignItems: "center",
marginBottom: "25px",
display: 'flex',
flexDirection: 'column',
marginTop: '15px',
width: '90%',
maxWidth: '1000px',