Browse Source

Merge pull request #18 from QortalSeth/main

Final updates to new Q-Tube 2.0 release
pull/19/head^2
Qortal Dev 7 months ago committed by GitHub
parent
commit
0fc8fb5c5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      package.json
  2. 48
      src/App.tsx
  3. 29
      src/components/PublishVideo/PublishVideo-styles.tsx
  4. 27
      src/components/PublishVideo/PublishVideo.tsx
  5. 44
      src/components/common/SubscribeButton.tsx
  6. 32
      src/hooks/useFetchVideos.tsx
  7. 18
      src/main.tsx
  8. 605
      src/pages/Home/Home.tsx
  9. 10
      src/pages/Home/VideoList-styles.tsx
  10. 75
      src/pages/IndividualProfile/IndividualProfile.tsx
  11. 2
      src/pages/PlaylistContent/PlaylistContent.tsx
  12. 5
      src/pages/VideoContent/VideoContent.tsx
  13. 34
      src/state/features/persistSlice.ts
  14. 11
      src/state/features/videoSlice.ts
  15. 4
      src/state/store.ts

2
package.json

@ -1,7 +1,7 @@
{ {
"name": "qtube", "name": "qtube",
"private": true, "private": true,
"version": "0.0.0", "version": "2.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

48
src/App.tsx

@ -1,8 +1,8 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Routes, Route } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline } from "@mui/material"; import { CssBaseline } from "@mui/material";
import { lightTheme, darkTheme } from "./styles/theme"; import { darkTheme, lightTheme } from "./styles/theme";
import { store } from "./state/store"; import { store } from "./state/store";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import GlobalWrapper from "./wrappers/GlobalWrapper"; import GlobalWrapper from "./wrappers/GlobalWrapper";
@ -14,6 +14,8 @@ import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/PlaylistContent/PlaylistContent"; import { PlaylistContent } from "./pages/PlaylistContent/PlaylistContent";
import { PersistGate } from "redux-persist/integration/react"; import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist"; import { persistStore } from "redux-persist";
import { setFilteredSubscriptions } from "./state/features/videoSlice.ts";
import { SubscriptionObject } from "./state/features/persistSlice.ts";
function App() { function App() {
// const themeColor = window._qdnTheme // const themeColor = window._qdnTheme
@ -21,6 +23,46 @@ function App() {
const [theme, setTheme] = useState("dark"); const [theme, setTheme] = useState("dark");
let persistor = persistStore(store); let persistor = persistStore(store);
const filterVideosByName = (
subscriptionList: SubscriptionObject[],
userName: string
) => {
return subscriptionList.filter(item => {
return item.userName === userName;
});
};
const getUserName = async () => {
const account = await qortalRequest({
action: "GET_USER_ACCOUNT",
});
const nameData = await qortalRequest({
action: "GET_ACCOUNT_NAMES",
address: account.address,
});
if (nameData?.length > 0) return nameData[0].name;
else return "";
};
const subscriptionListFilter = async () => {
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;
};
useEffect(() => {
const subscriptionList = store.getState().persist.subscriptionList;
subscriptionListFilter().then(filteredList => {
store.dispatch(setFilteredSubscriptions(filteredList));
});
}, []);
return ( return (
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>

29
src/components/PublishVideo/PublishVideo-styles.tsx

@ -9,7 +9,7 @@ import {
Rating, Rating,
TextField, TextField,
Typography, Typography,
Select Select,
} from "@mui/material"; } from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../assets/svgs/TimesSVG"; import { TimesSVG } from "../../assets/svgs/TimesSVG";
@ -51,6 +51,9 @@ export const CreateContainer = styled(Box)(({ theme }) => ({
borderRadius: "50%", borderRadius: "50%",
})); }));
export const CodecTypography = styled(Typography)(({ theme }) => ({
fontSize: "18px",
}));
export const ModalBody = styled(Box)(({ theme }) => ({ export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute", position: "absolute",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
@ -159,8 +162,6 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({
}, },
})); }));
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({ export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse", fontFamily: "Copse",
letterSpacing: "1px", letterSpacing: "1px",
@ -539,8 +540,8 @@ export const NoReviewsFont = styled(Typography)(({ theme }) => ({
export const StyledButton = styled(Button)(({ theme }) => ({ export const StyledButton = styled(Button)(({ theme }) => ({
fontWeight: 600, fontWeight: 600,
color: theme.palette.text.primary color: theme.palette.text.primary,
})) }));
export const CustomSelect = styled(Select)(({ theme }) => ({ export const CustomSelect = styled(Select)(({ theme }) => ({
fontFamily: "Mulish", fontFamily: "Mulish",
@ -549,34 +550,34 @@ export const CustomSelect = styled(Select)(({ theme }) => ({
fontWeight: 400, fontWeight: 400,
color: theme.palette.text.primary, color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
'& .MuiSelect-select': { "& .MuiSelect-select": {
padding: '12px', padding: "12px",
fontFamily: "Mulish", fontFamily: "Mulish",
fontSize: "19px", fontSize: "19px",
letterSpacing: "0px", letterSpacing: "0px",
fontWeight: 400, fontWeight: 400,
borderRadius: theme.shape.borderRadius, // Match border radius borderRadius: theme.shape.borderRadius, // Match border radius
}, },
'&:before': { "&:before": {
// Underline style // Underline style
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf", borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
}, },
'&:after': { "&:after": {
// Underline style when focused // Underline style when focused
borderBottomColor: theme.palette.secondary.main, borderBottomColor: theme.palette.secondary.main,
}, },
'& .MuiOutlinedInput-root': { "& .MuiOutlinedInput-root": {
'& fieldset': { "& fieldset": {
borderColor: "#E0E3E7", borderColor: "#E0E3E7",
}, },
'&:hover fieldset': { "&:hover fieldset": {
borderColor: "#B2BAC2", borderColor: "#B2BAC2",
}, },
'&.Mui-focused fieldset': { "&.Mui-focused fieldset": {
borderColor: "#6F7E8C", borderColor: "#6F7E8C",
}, },
}, },
'& .MuiInputBase-root': { "& .MuiInputBase-root": {
fontFamily: "Mulish", fontFamily: "Mulish",
fontSize: "19px", fontSize: "19px",
letterSpacing: "0px", letterSpacing: "0px",

27
src/components/PublishVideo/PublishVideo.tsx

@ -3,6 +3,7 @@ import Compressor from "compressorjs";
import { import {
AddCoverImageButton, AddCoverImageButton,
AddLogoIcon, AddLogoIcon,
CodecTypography,
CoverImagePreview, CoverImagePreview,
CrowdfundActionButton, CrowdfundActionButton,
CrowdfundActionButtonRow, CrowdfundActionButtonRow,
@ -713,15 +714,23 @@ export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
<Typography sx={{fontSize: "14px"}}> <CodecTypography>
Supported File Containers: MP4, Ogg, WebM, WAV Supported File Containers:{" "}
</Typography> <span style={{ fontWeight: "bold" }}>MP4</span>, Ogg, WebM,
<Typography sx={{fontSize: "14px"}}> WAV
Audio Codecs: FLAC, MP3, Opus, PCM (8/16/32-bit, μ-law), Vorbis </CodecTypography>
</Typography> <CodecTypography>
<Typography sx={{fontSize: "14px"}}> Audio Codecs: <span style={{ fontWeight: "bold" }}>Opus</span>
Video Codecs: AV1, VP8, VP9 , MP3, FLAC, PCM (8/16/32-bit, μ-law), Vorbis
</Typography> </CodecTypography>
<CodecTypography>
Video Codecs: <span style={{ fontWeight: "bold" }}>AV1</span>,
VP8, VP9, H.264
</CodecTypography>
<CodecTypography sx={{ fontWeight: "800", color: "red" }}>
Using unsupported Codecs may result in video or audio not
working properly
</CodecTypography>
</Box> </Box>
<Box <Box

44
src/components/common/SubscribeButton.tsx

@ -2,29 +2,57 @@ import { Button, ButtonProps } from "@mui/material";
import { MouseEvent, useEffect, useState } from "react"; import { MouseEvent, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts"; import { RootState } from "../../state/store.ts";
import { subscribe, unSubscribe } from "../../state/features/persistSlice.ts"; import {
resetSubscriptions,
subscribe,
unSubscribe,
} from "../../state/features/persistSlice.ts";
import { setFilteredSubscriptions } from "../../state/features/videoSlice.ts";
interface SubscribeButtonProps extends ButtonProps { interface SubscribeButtonProps extends ButtonProps {
name: string; subscriberName: string;
} }
export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => { export const SubscribeButton = ({
subscriberName,
...props
}: SubscribeButtonProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const persistSelector = useSelector((state: RootState) => { const filteredSubscriptionList = useSelector((state: RootState) => {
return state.persist; return state.video.filteredSubscriptionList;
}); });
const userName = useSelector((state: RootState) => state.auth.user?.name);
const [isSubscribed, setIsSubscribed] = useState<boolean>(false); const [isSubscribed, setIsSubscribed] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setIsSubscribed(persistSelector.subscriptionList.includes(name)); const isSubscribedToName =
filteredSubscriptionList.find(item => {
return item.subscriberName === subscriberName;
}) !== undefined;
setIsSubscribed(isSubscribedToName);
}, []); }, []);
const subscriptionData = {
userName: userName,
subscriberName: subscriberName,
};
const subscribeToRedux = () => { const subscribeToRedux = () => {
dispatch(subscribe(name)); dispatch(subscribe(subscriptionData));
dispatch(
setFilteredSubscriptions([...filteredSubscriptionList, subscriptionData])
);
setIsSubscribed(true); setIsSubscribed(true);
}; };
const unSubscribeFromRedux = () => { const unSubscribeFromRedux = () => {
dispatch(unSubscribe(name)); dispatch(unSubscribe(subscriptionData));
dispatch(
setFilteredSubscriptions(
filteredSubscriptionList.filter(
item => item.subscriberName !== subscriptionData.subscriberName
)
)
);
setIsSubscribed(false); setIsSubscribed(false);
}; };

32
src/hooks/useFetchVideos.tsx

@ -25,6 +25,7 @@ import {
QTUBE_VIDEO_BASE, QTUBE_VIDEO_BASE,
} from "../constants/Identifiers.ts"; } from "../constants/Identifiers.ts";
import { allTabValue, subscriptionTabValue } from "../constants/Misc.ts"; import { allTabValue, subscriptionTabValue } from "../constants/Misc.ts";
import { persistReducer } from "redux-persist";
export const useFetchVideos = () => { export const useFetchVideos = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -48,9 +49,7 @@ export const useFetchVideos = () => {
(state: RootState) => state.video.filteredVideos (state: RootState) => state.video.filteredVideos
); );
const subscriptions = useSelector( const videoReducer = useSelector((state: RootState) => state.video);
(state: RootState) => state.persist.subscriptionList
);
const checkAndUpdateVideo = React.useCallback( const checkAndUpdateVideo = React.useCallback(
(video: Video) => { (video: Video) => {
@ -189,22 +188,38 @@ export const useFetchVideos = () => {
} }
}, [videos, hashMapVideos]); }, [videos, hashMapVideos]);
type FilterType = {
name?: string;
category?: string;
subcategory?: string;
keywords?: string;
type?: string;
};
const emptyFilters = {
name: "",
category: "",
subcategory: "",
keywords: "",
type: "",
};
const getVideos = React.useCallback( const getVideos = React.useCallback(
async ( async (
filters = {}, filters = emptyFilters,
reset?: boolean, reset?: boolean,
resetFilters?: boolean, resetFilters?: boolean,
limit?: number, limit?: number,
listType = allTabValue listType = allTabValue
) => { ) => {
emptyFilters.type = filters.type;
try { try {
const { const {
name = "", name = "",
category = "", category = "",
subcategory = "", subcategory = "",
keywords = "", keywords = "",
type = "", type = filters.type,
}: any = resetFilters ? {} : filters; }: FilterType = resetFilters ? emptyFilters : filters;
let offset = videos.length; let offset = videos.length;
if (reset) { if (reset) {
offset = 0; offset = 0;
@ -217,8 +232,9 @@ export const useFetchVideos = () => {
defaultUrl = defaultUrl + `&name=${name}`; defaultUrl = defaultUrl + `&name=${name}`;
} }
if (listType === subscriptionTabValue) { if (listType === subscriptionTabValue) {
subscriptions.map(sub => { const filteredSubscribeList = videoReducer.filteredSubscriptionList;
defaultUrl += `&name=${sub}`; filteredSubscribeList.map(sub => {
defaultUrl += `&name=${sub.subscriberName}`;
}); });
} }

18
src/main.tsx

@ -1,17 +1,17 @@
import ReactDOM from 'react-dom/client' import ReactDOM from "react-dom/client";
import App from './App' import App from "./App";
import './index.css' import "./index.css";
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from "react-router-dom";
interface CustomWindow extends Window { interface CustomWindow extends Window {
_qdnBase: string _qdnBase: string;
} }
const customWindow = window as unknown as CustomWindow const customWindow = window as unknown as CustomWindow;
const baseUrl = customWindow?._qdnBase || '' const baseUrl = customWindow?._qdnBase || "";
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<BrowserRouter basename={baseUrl}> <BrowserRouter basename={baseUrl}>
<App /> <App />
<div id="modal-root" /> <div id="modal-root" />
</BrowserRouter> </BrowserRouter>
) );

605
src/pages/Home/Home.tsx

@ -25,6 +25,7 @@ import {
FiltersSubContainer, FiltersSubContainer,
ProductManagerRow, ProductManagerRow,
FiltersRadioButton, FiltersRadioButton,
StatsCol,
} from "./VideoList-styles"; } from "./VideoList-styles";
import { SubtitleContainer } from "./Home-styles"; import { SubtitleContainer } from "./Home-styles";
@ -34,7 +35,10 @@ import {
changefilterName, changefilterName,
changefilterSearch, changefilterSearch,
} from "../../state/features/videoSlice"; } from "../../state/features/videoSlice";
import { changeFilterType } from "../../state/features/persistSlice.ts"; import {
changeFilterType,
resetSubscriptions,
} from "../../state/features/persistSlice.ts";
import { categories, subCategories } from "../../constants/Categories.ts"; import { categories, subCategories } from "../../constants/Categories.ts";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx"; import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { TabContext, TabList, TabPanel } from "@mui/lab"; import { TabContext, TabList, TabPanel } from "@mui/lab";
@ -53,7 +57,7 @@ export const Home = ({ mode }: HomeProps) => {
const filterValue = useSelector( const filterValue = useSelector(
(state: RootState) => state.video.filterValue (state: RootState) => state.video.filterValue
); );
const persistSelector = useSelector((state: RootState) => state.persist); const persistReducer = useSelector((state: RootState) => state.persist);
const filterType = useSelector( const filterType = useSelector(
(state: RootState) => state.persist.filterType (state: RootState) => state.persist.filterType
); );
@ -74,12 +78,12 @@ export const Home = ({ mode }: HomeProps) => {
(state: RootState) => state.global.videosPerNamePublished (state: RootState) => state.global.videosPerNamePublished
); );
const { videos: globalVideos } = useSelector( const { videos: globalVideos, filteredSubscriptionList } = useSelector(
(state: RootState) => state.video (state: RootState) => state.video
); );
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>(persistSelector.selectedTab); const [tabValue, setTabValue] = useState<string>(persistReducer.selectedTab);
const tabFontSize = "20px"; const tabFontSize = "20px";
@ -120,8 +124,13 @@ export const Home = ({ mode }: HomeProps) => {
const afterFetch = useRef(false); const afterFetch = useRef(false);
const isFetching = useRef(false); const isFetching = useRef(false);
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered, getVideosCount } = const {
useFetchVideos(); getVideos,
getNewVideos,
checkNewVideos,
getVideosFiltered,
getVideosCount,
} = useFetchVideos();
const getVideosHandler = React.useCallback( const getVideosHandler = React.useCallback(
async (reset?: boolean, resetFilters?: boolean) => { async (reset?: boolean, resetFilters?: boolean) => {
@ -170,7 +179,19 @@ export const Home = ({ mode }: HomeProps) => {
firstFetch.current = true; firstFetch.current = true;
setIsLoading(true); setIsLoading(true);
await getVideos({ type: filterType }, null, null, 20, tabValue); await getVideos(
{
name: "",
category: "",
subcategory: "",
keywords: "",
type: filterType,
},
null,
null,
20,
tabValue
);
afterFetch.current = true; afterFetch.current = true;
isFetching.current = false; isFetching.current = false;
@ -227,7 +248,6 @@ export const Home = ({ mode }: HomeProps) => {
}, [getVideosHandlerMount, globalVideos]); }, [getVideosHandlerMount, globalVideos]);
const filtersToDefault = async () => { const filtersToDefault = async () => {
setFilterType("videos");
setFilterSearch(""); setFilterSearch("");
setFilterName(""); setFilterName("");
setSelectedCategoryVideos(null); setSelectedCategoryVideos(null);
@ -273,293 +293,302 @@ export const Home = ({ mode }: HomeProps) => {
return ( return (
<> <>
<Box sx={{ width: "100%" }}> <Grid container sx={{ width: "100%" }}>
<Grid container spacing={2} justifyContent="space-around"> <FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}>
<Grid item xs={12} sm={4}> <FiltersContainer>
Total Videos Published: {totalVideosPublished} <StatsCol
</Grid> sx={{ display: persistReducer.showStats ? "block" : "none" }}
<Grid item xs={12} sm={4}> >
Total Names Publishing: {totalNamesPublished} <div>
</Grid> # of Videos:{" "}
<Grid item xs={12} sm={4}> <span style={{ fontWeight: "bold" }}>
Average Videos per Name: {videosPerNamePublished} {totalVideosPublished}
</Grid> </span>
</Grid> </div>
</Box> <div>
<Grid container sx={{ width: "100%" }}> Names Publishing:{" "}
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}> <span style={{ fontWeight: "bold" }}>
<FiltersContainer> {totalNamesPublished}
<Input </span>
id="standard-adornment-name" </div>
onChange={e => { <div>
setFilterSearch(e.target.value); Videos per Name:{" "}
}} <span style={{ fontWeight: "bold" }}>
value={filterSearch} {videosPerNamePublished}
placeholder="Search" </span>
onKeyDown={handleInputKeyDown} </div>
sx={{ </StatsCol>
borderBottom: "1px solid white", <Input
"&&:before": { id="standard-adornment-name"
borderBottom: "none", onChange={e => {
}, setFilterSearch(e.target.value);
"&&:after": { }}
borderBottom: "none", value={filterSearch}
}, placeholder="Search"
"&&:hover:before": { onKeyDown={handleInputKeyDown}
borderBottom: "none", sx={{
}, borderBottom: "1px solid white",
"&&.Mui-focused:before": { "&&:before": {
borderBottom: "none", borderBottom: "none",
}, },
"&&.Mui-focused": { "&&:after": {
outline: "none", borderBottom: "none",
}, },
fontSize: "18px", "&&:hover:before": {
}} borderBottom: "none",
/> },
<Input "&&.Mui-focused:before": {
id="standard-adornment-name" borderBottom: "none",
onChange={e => { },
setFilterName(e.target.value); "&&.Mui-focused": {
}} outline: "none",
value={filterName} },
placeholder="User's Name (Exact)" fontSize: "18px",
onKeyDown={handleInputKeyDown} }}
sx={{ />
marginTop: "20px", <Input
borderBottom: "1px solid white", id="standard-adornment-name"
"&&:before": { onChange={e => {
borderBottom: "none", setFilterName(e.target.value);
}, }}
"&&:after": { value={filterName}
borderBottom: "none", placeholder="User's Name (Exact)"
}, onKeyDown={handleInputKeyDown}
"&&:hover:before": { sx={{
borderBottom: "none", marginTop: "20px",
}, borderBottom: "1px solid white",
"&&.Mui-focused:before": { "&&:before": {
borderBottom: "none", borderBottom: "none",
}, },
"&&.Mui-focused": { "&&:after": {
outline: "none", borderBottom: "none",
}, },
fontSize: "18px", "&&:hover:before": {
}} borderBottom: "none",
/> },
"&&.Mui-focused:before": {
<FiltersSubContainer> borderBottom: "none",
<FormControl sx={{ width: "100%", marginTop: "30px" }}> },
<Box "&&.Mui-focused": {
sx={{ outline: "none",
display: "flex", },
gap: "20px", fontSize: "18px",
alignItems: "center", }}
flexDirection: "column", />
}}
> <FiltersSubContainer>
<FormControl fullWidth sx={{ marginBottom: 1 }}> <FormControl sx={{ width: "100%", marginTop: "30px" }}>
<InputLabel <Box
sx={{ sx={{
fontSize: "16px", display: "flex",
}} gap: "20px",
id="Category" alignItems: "center",
> flexDirection: "column",
Category }}
</InputLabel> >
<Select <FormControl fullWidth sx={{ marginBottom: 1 }}>
labelId="Category" <InputLabel
input={<OutlinedInput label="Category" />} sx={{
value={selectedCategoryVideos?.id || ""} fontSize: "16px",
onChange={handleOptionCategoryChangeVideos} }}
sx={{ id="Category"
// Target the input field >
".MuiSelect-select": { Category
fontSize: "16px", // Change font size for the selected value </InputLabel>
padding: "10px 5px 15px 15px;", <Select
}, labelId="Category"
// Target the dropdown icon input={<OutlinedInput label="Category" />}
".MuiSelect-icon": { value={selectedCategoryVideos?.id || ""}
fontSize: "20px", // Adjust if needed onChange={handleOptionCategoryChangeVideos}
}, sx={{
// Target the dropdown menu // Target the input field
"& .MuiMenu-paper": { ".MuiSelect-select": {
".MuiMenuItem-root": { fontSize: "16px", // Change font size for the selected value
fontSize: "14px", // Change font size for the menu items padding: "10px 5px 15px 15px;",
}, },
}, // Target the dropdown icon
}} ".MuiSelect-icon": {
> fontSize: "20px", // Adjust if needed
{categories.map(option => ( },
<MenuItem key={option.id} value={option.id}> // Target the dropdown menu
{option.name} "& .MuiMenu-paper": {
</MenuItem> ".MuiMenuItem-root": {
))} fontSize: "14px", // Change font size for the menu items
</Select>
</FormControl>
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Sub-Category"
>
Sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={<OutlinedInput label="Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={e =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
sx={{
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
},
// Target the dropdown icon
".MuiSelect-icon": {
fontSize: "20px", // Adjust if needed
}, },
// Target the dropdown menu },
"& .MuiMenu-paper": { }}
".MuiMenuItem-root": { >
fontSize: "14px", // Change font size for the menu items {categories.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Sub-Category"
>
Sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={<OutlinedInput label="Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={e =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
sx={{
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
}, },
}, // Target the dropdown icon
}} ".MuiSelect-icon": {
> fontSize: "20px", // Adjust if needed
{subCategories[selectedCategoryVideos.id].map( },
option => ( // Target the dropdown menu
<MenuItem key={option.id} value={option.id}> "& .MuiMenu-paper": {
{option.name} ".MuiMenuItem-root": {
</MenuItem> fontSize: "14px", // Change font size for the menu items
) },
)} },
</Select> }}
</FormControl> >
)} {subCategories[selectedCategoryVideos.id].map(
</Box> option => (
</FormControl> <MenuItem key={option.id} value={option.id}>
</FiltersSubContainer> {option.name}
<FiltersSubContainer> </MenuItem>
<FiltersRow> )
Videos )}
<FiltersRadioButton </Select>
checked={filterType === "videos"} </FormControl>
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { )}
setFilterType("videos"); </Box>
}} </FormControl>
inputProps={{ "aria-label": "controlled" }} </FiltersSubContainer>
/> <FiltersSubContainer>
</FiltersRow> <FiltersRow>
<FiltersRow> Videos
Playlists <FiltersRadioButton
<FiltersRadioButton checked={filterType === "videos"}
checked={filterType === "playlists"} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setFilterType("videos");
setFilterType("playlists"); }}
}} inputProps={{ "aria-label": "controlled" }}
inputProps={{ "aria-label": "controlled" }} />
/> </FiltersRow>
</FiltersRow> <FiltersRow>
</FiltersSubContainer> Playlists
<Button <FiltersRadioButton
onClick={() => { checked={filterType === "playlists"}
filtersToDefault(); onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
}} setFilterType("playlists");
sx={{ }}
marginTop: "20px", inputProps={{ "aria-label": "controlled" }}
}} />
variant="contained" </FiltersRow>
> </FiltersSubContainer>
reset <Button
</Button> onClick={() => {
<Button filtersToDefault();
onClick={() => { }}
getVideosHandler(true); sx={{
}} marginTop: "20px",
sx={{ }}
marginTop: "20px", variant="contained"
}} >
variant="contained" reset
> </Button>
Search <Button
</Button> onClick={() => {
</FiltersContainer> getVideosHandler(true);
</FiltersCol> }}
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}> sx={{
<ProductManagerRow> marginTop: "20px",
<Box }}
sx={{ variant="contained"
width: "100%", >
display: "flex", Search
flexDirection: "column", </Button>
alignItems: "center", </FiltersContainer>
marginTop: "20px", </FiltersCol>
}} <Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
> <ProductManagerRow>
<SubtitleContainer <Box
sx={{ sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%", width: "100%",
maxWidth: "1400px", display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}} }}
></SubtitleContainer> >
<TabContext value={tabValue}> <SubtitleContainer
<TabList sx={{
onChange={changeTab} justifyContent: "flex-start",
textColor={"secondary"} paddingLeft: "15px",
indicatorColor={"secondary"} width: "100%",
> maxWidth: "1400px",
<Tab }}
label="All Videos" ></SubtitleContainer>
value={allTabValue} <TabContext value={tabValue}>
sx={{ fontSize: tabFontSize }} <TabList
/> onChange={changeTab}
<Tab textColor={"secondary"}
label="Subscriptions" indicatorColor={"secondary"}
value={subscriptionTabValue} >
sx={{ fontSize: tabFontSize }} <Tab
/> label="All Videos"
</TabList> value={allTabValue}
<TabPanel value={allTabValue} sx={{ width: "100%" }}> sx={{ fontSize: tabFontSize }}
<VideoList videos={videos} /> />
<LazyLoad <Tab
onLoadMore={getVideosHandler} label="Subscriptions"
isLoading={isLoading} value={subscriptionTabValue}
></LazyLoad> sx={{ fontSize: tabFontSize }}
</TabPanel> />
<TabPanel value={subscriptionTabValue} sx={{ width: "100%" }}> </TabList>
{persistSelector.subscriptionList.length > 0 ? ( <TabPanel value={allTabValue} sx={{ width: "100%" }}>
<> <VideoList videos={videos} />
<VideoList videos={videos} /> <LazyLoad
<LazyLoad onLoadMore={getVideosHandler}
onLoadMore={getVideosHandler} isLoading={isLoading}
isLoading={isLoading} ></LazyLoad>
></LazyLoad> </TabPanel>
</> <TabPanel value={subscriptionTabValue} sx={{ width: "100%" }}>
) : ( {filteredSubscriptionList.length > 0 ? (
<div style={{ textAlign: "center" }}> <>
You have no subscriptions <VideoList videos={videos} />
</div> <LazyLoad
)} onLoadMore={getVideosHandler}
</TabPanel> isLoading={isLoading}
</TabContext> ></LazyLoad>
</Box> </>
</ProductManagerRow> ) : (
<div style={{ textAlign: "center" }}>
You have no subscriptions
</div>
)}
</TabPanel>
</TabContext>
</Box>
</ProductManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid> </Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
</> </>
); );
}; };

10
src/pages/Home/VideoList-styles.tsx

@ -237,6 +237,16 @@ export const FiltersCol = styled(Grid)(({ theme }) => ({
borderRight: `1px solid ${theme.palette.background.paper}`, borderRight: `1px solid ${theme.palette.background.paper}`,
})); }));
export const StatsCol = styled(Grid)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "100%",
padding: "20px 15px",
backgroundColor: theme.palette.background.default,
borderTop: `1px solid ${theme.palette.background.paper}`,
borderRight: `1px solid ${theme.palette.background.paper}`,
}));
export const FiltersContainer = styled(Box)(({ theme }) => ({ export const FiltersContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",

75
src/pages/IndividualProfile/IndividualProfile.tsx

@ -1,65 +1,74 @@
import React, { useMemo } from 'react' import React, { useMemo } from "react";
import { VideoListComponentLevel } from '../Home/VideoListComponentLevel' import { VideoListComponentLevel } from "../Home/VideoListComponentLevel";
import { HeaderContainer, ProfileContainer } from './Profile-styles' import { HeaderContainer, ProfileContainer } from "./Profile-styles";
import { AuthorTextComment, StyledCardColComment, StyledCardHeaderComment } from '../VideoContent/VideoContent-styles' import {
import { Avatar, Box, useTheme } from '@mui/material' AuthorTextComment,
import { useParams } from 'react-router-dom' StyledCardColComment,
import { useSelector } from 'react-redux' StyledCardHeaderComment,
import { setUserAvatarHash } from '../../state/features/globalSlice' } from "../VideoContent/VideoContent-styles";
import { RootState } from '../../state/store' import { Avatar, Box, useTheme } from "@mui/material";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { setUserAvatarHash } from "../../state/features/globalSlice";
import { RootState } from "../../state/store";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx"; import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
export const IndividualProfile = () => { export const IndividualProfile = () => {
const { name: paramName } = useParams() const { name: paramName } = useParams();
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
const theme = useTheme() const theme = useTheme();
const avatarUrl = useMemo(() => {
let url = "";
const avatarUrl = useMemo(()=> { if (paramName && userAvatarHash[paramName]) {
let url = '' url = userAvatarHash[paramName];
if(paramName && userAvatarHash[paramName]){
url = userAvatarHash[paramName]
} }
return url return url;
}, [userAvatarHash, paramName]) }, [userAvatarHash, paramName]);
return ( return (
<ProfileContainer> <ProfileContainer>
<HeaderContainer> <HeaderContainer>
<Box sx={{ <Box
cursor: 'pointer' sx={{
}} > cursor: "pointer",
}}
>
<StyledCardHeaderComment <StyledCardHeaderComment
sx={{ sx={{
'& .MuiCardHeader-content': { "& .MuiCardHeader-content": {
overflow: 'hidden' overflow: "hidden",
} },
}} }}
> >
<Box> <Box>
<Avatar src={`/arbitrary/THUMBNAIL/${paramName}/qortal_avatar`} alt={`${paramName}'s avatar`} /> <Avatar
src={`/arbitrary/THUMBNAIL/${paramName}/qortal_avatar`}
alt={`${paramName}'s avatar`}
/>
</Box> </Box>
<StyledCardColComment> <StyledCardColComment>
<AuthorTextComment <AuthorTextComment
color={ color={
theme.palette.mode === 'light' theme.palette.mode === "light"
? theme.palette.text.secondary ? theme.palette.text.secondary
: '#d6e8ff' : "#d6e8ff"
} }
> >
{paramName} {paramName}
</AuthorTextComment> </AuthorTextComment>
</StyledCardColComment> </StyledCardColComment>
<SubscribeButton name={paramName} sx={{marginLeft:'10px'}}/> <SubscribeButton
subscriberName={paramName}
sx={{ marginLeft: "10px" }}
/>
</StyledCardHeaderComment> </StyledCardHeaderComment>
</Box> </Box>
</HeaderContainer> </HeaderContainer>
<VideoListComponentLevel /> <VideoListComponentLevel />
</ProfileContainer> </ProfileContainer>
);
) };
}

2
src/pages/PlaylistContent/PlaylistContent.tsx

@ -497,7 +497,7 @@ export const PlaylistContent = () => {
> >
{name} {name}
<SubscribeButton <SubscribeButton
name={name} subscriberName={name}
sx={{ marginLeft: "20px" }} sx={{ marginLeft: "20px" }}
/> />
</AuthorTextComment> </AuthorTextComment>

5
src/pages/VideoContent/VideoContent.tsx

@ -437,7 +437,10 @@ export const VideoContent = () => {
}} }}
> >
{name} {name}
<SubscribeButton name={name} sx={{ marginLeft: "20px" }} /> <SubscribeButton
subscriberName={name}
sx={{ marginLeft: "20px" }}
/>
</AuthorTextComment> </AuthorTextComment>
</StyledCardColComment> </StyledCardColComment>
</StyledCardHeaderComment> </StyledCardHeaderComment>

34
src/state/features/persistSlice.ts

@ -2,12 +2,21 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { subscriptionTabValue } from "../../constants/Misc.ts"; import { subscriptionTabValue } from "../../constants/Misc.ts";
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
type SubscriptionListFilterType = "ALL" | "currentNameOnly";
export type SubscriptionObject = {
userName: string;
subscriberName: string;
};
interface settingsState { interface settingsState {
selectedTab: string; selectedTab: string;
stretchVideoSetting: StretchVideoType; stretchVideoSetting: StretchVideoType;
filterType: string; filterType: string;
subscriptionList: string[]; subscriptionList: SubscriptionObject[];
playbackRate: number; playbackRate: number;
subscriptionListFilter: SubscriptionListFilterType;
showStats: boolean;
} }
const initialState: settingsState = { const initialState: settingsState = {
@ -16,6 +25,8 @@ const initialState: settingsState = {
filterType: "videos", filterType: "videos",
subscriptionList: [], subscriptionList: [],
playbackRate: 1, playbackRate: 1,
subscriptionListFilter: "currentNameOnly",
showStats: true,
}; };
export const persistSlice = createSlice({ export const persistSlice = createSlice({
@ -28,16 +39,28 @@ export const persistSlice = createSlice({
setStretchVideoSetting: (state, action) => { setStretchVideoSetting: (state, action) => {
state.stretchVideoSetting = action.payload; state.stretchVideoSetting = action.payload;
}, },
subscribe: (state, action: PayloadAction<string>) => { setShowStats: (state, action) => {
state.showStats = action.payload;
},
subscribe: (state, action: PayloadAction<SubscriptionObject>) => {
const currentSubscriptions = state.subscriptionList; const currentSubscriptions = state.subscriptionList;
if (!currentSubscriptions.includes(action.payload)) { const notSubscribedToName =
currentSubscriptions.find(item => {
return item.subscriberName === action.payload.subscriberName;
}) === undefined;
if (notSubscribedToName) {
state.subscriptionList = [...currentSubscriptions, action.payload]; state.subscriptionList = [...currentSubscriptions, action.payload];
} }
console.log("subscribeList after subscribe: ", state.subscriptionList);
}, },
unSubscribe: (state, action) => { unSubscribe: (state, action: PayloadAction<SubscriptionObject>) => {
state.subscriptionList = state.subscriptionList.filter( state.subscriptionList = state.subscriptionList.filter(
item => item !== action.payload item => item.subscriberName !== action.payload.subscriberName
); );
console.log("subscribeList after unsubscribe: ", state.subscriptionList);
},
resetSubscriptions: state => {
state.subscriptionList = [];
}, },
setReduxPlaybackRate: (state, action) => { setReduxPlaybackRate: (state, action) => {
state.playbackRate = action.payload; state.playbackRate = action.payload;
@ -54,6 +77,7 @@ export const {
unSubscribe, unSubscribe,
setReduxPlaybackRate, setReduxPlaybackRate,
changeFilterType, changeFilterType,
resetSubscriptions,
} = persistSlice.actions; } = persistSlice.actions;
export default persistSlice.reducer; export default persistSlice.reducer;

11
src/state/features/videoSlice.ts

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SubscriptionObject } from "./persistSlice.ts";
interface GlobalState { interface GlobalState {
videos: Video[]; videos: Video[];
@ -14,6 +15,7 @@ interface GlobalState {
selectedSubCategoryVideos: any; selectedSubCategoryVideos: any;
editVideoProperties: any; editVideoProperties: any;
editPlaylistProperties: any; editPlaylistProperties: any;
filteredSubscriptionList: SubscriptionObject[];
} }
const initialState: GlobalState = { const initialState: GlobalState = {
@ -30,6 +32,7 @@ const initialState: GlobalState = {
selectedSubCategoryVideos: null, selectedSubCategoryVideos: null,
editVideoProperties: null, editVideoProperties: null,
editPlaylistProperties: null, editPlaylistProperties: null,
filteredSubscriptionList: [],
}; };
export interface Video { export interface Video {
@ -168,6 +171,13 @@ export const videoSlice = createSlice({
state.videos = state.videos.filter(item => item.user !== username); state.videos = state.videos.filter(item => item.user !== username);
}, },
setFilteredSubscriptions: (
state,
action: PayloadAction<SubscriptionObject[]>
) => {
state.filteredSubscriptionList = action.payload;
},
}, },
}); });
@ -196,6 +206,7 @@ export const {
setEditVideo, setEditVideo,
setEditPlaylist, setEditPlaylist,
addtoHashMapSuperlikes, addtoHashMapSuperlikes,
setFilteredSubscriptions,
} = videoSlice.actions; } = videoSlice.actions;
export default videoSlice.reducer; export default videoSlice.reducer;

4
src/state/store.ts

@ -3,7 +3,7 @@ import notificationsReducer from "./features/notificationsSlice";
import authReducer from "./features/authSlice"; import authReducer from "./features/authSlice";
import globalReducer from "./features/globalSlice"; import globalReducer from "./features/globalSlice";
import videoReducer from "./features/videoSlice"; import videoReducer from "./features/videoSlice";
import settingsReducer from "./features/persistSlice.ts"; import persistDataReducer from "./features/persistSlice.ts";
import { import {
persistReducer, persistReducer,
FLUSH, FLUSH,
@ -26,7 +26,7 @@ const reducer = combineReducers({
auth: authReducer, auth: authReducer,
global: globalReducer, global: globalReducer,
video: videoReducer, video: videoReducer,
persist: persistReducer(persistSettingsConfig, settingsReducer), persist: persistReducer(persistSettingsConfig, persistDataReducer),
}); });
export const store = configureStore({ export const store = configureStore({

Loading…
Cancel
Save