Update to the playlist picker
All checks were successful
Deploy to production / build (push) Successful in 20s
All checks were successful
Deploy to production / build (push) Successful in 20s
This commit is contained in:
@@ -121,8 +121,6 @@ sudo pm2 start server.cjs
|
|||||||
- Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led)
|
- Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led)
|
||||||
- Update Theme playlists
|
- Update Theme playlists
|
||||||
- Include mantine-reflection (https://gfazioli.github.io/mantine-reflection#example-complex-components)
|
- Include mantine-reflection (https://gfazioli.github.io/mantine-reflection#example-complex-components)
|
||||||
- Include mantine-parallax (https://gfazioli.github.io/mantine-parallax#example-presentation)
|
|
||||||
- Include mantine-depth-select (https://gfazioli.github.io/mantine-depth-select/)
|
|
||||||
- Explore https://gfazioli.github.io/mantine-scene/
|
- Explore https://gfazioli.github.io/mantine-scene/
|
||||||
- Explore https://gfazioli.github.io/mantine-window/
|
- Explore https://gfazioli.github.io/mantine-window/
|
||||||
- Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random)
|
- Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random)
|
||||||
631
package-lock.json
generated
631
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -10,22 +10,24 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gfazioli/mantine-border-animate": "^3.0.2",
|
"@gfazioli/mantine-border-animate": "^3.0.5",
|
||||||
"@gfazioli/mantine-parallax": "^4.0.10",
|
"@gfazioli/mantine-depth-select": "^2.0.15",
|
||||||
"@gfazioli/mantine-text-animate": "^4.0.10",
|
"@gfazioli/mantine-parallax": "^4.0.13",
|
||||||
|
"@gfazioli/mantine-reflection": "^3.0.13",
|
||||||
|
"@gfazioli/mantine-text-animate": "^4.0.15",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@mantine/core": "^9.6.2",
|
"@mantine/core": "^9.7.1",
|
||||||
"@mantine/dates": "^9.6.2",
|
"@mantine/dates": "^9.7.1",
|
||||||
"@mantine/form": "^9.6.2",
|
"@mantine/form": "^9.7.1",
|
||||||
"@mantine/hooks": "^9.6.2",
|
"@mantine/hooks": "^9.7.1",
|
||||||
"@mantine/notifications": "^9.6.2",
|
"@mantine/notifications": "^9.7.1",
|
||||||
"@tanstack/react-query": "^5.103.2",
|
"@tanstack/react-query": "^5.104.1",
|
||||||
"@tanstack/react-query-devtools": "^5.103.2",
|
"@tanstack/react-query-devtools": "^5.104.1",
|
||||||
"@tanstack/react-query-persist-client": "^5.103.2",
|
"@tanstack/react-query-persist-client": "^5.104.1",
|
||||||
"@tanstack/react-table": "^9.2.4",
|
"@tanstack/react-table": "^9.2.8",
|
||||||
"@tanstack/react-virtual": "^3.14.13",
|
"@tanstack/react-virtual": "^3.14.14",
|
||||||
"idb-keyval": "^6.3.0",
|
"idb-keyval": "^6.3.0",
|
||||||
"jotai": "^3.0.0",
|
"jotai": "^3.0.1",
|
||||||
"openmeteo": "^1.2.3",
|
"openmeteo": "^1.2.3",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
@@ -35,16 +37,16 @@
|
|||||||
"recharts": "^3.10.1"
|
"recharts": "^3.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.6.2",
|
"@types/node": "^26.6.4",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.3.0",
|
||||||
"@vitejs/plugin-react": "^6.1.1",
|
"@vitejs/plugin-react": "^6.1.2",
|
||||||
"@vitest/coverage-v8": "^5.0.2",
|
"@vitest/coverage-v8": "^5.0.3",
|
||||||
"@vitest/ui": "^5.0.2",
|
"@vitest/ui": "^5.0.3",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vite": "^8.3.1",
|
"vite": "^8.3.4",
|
||||||
"vite-plugin-dts": "~5.1.1",
|
"vite-plugin-dts": "~5.1.2",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "^5.0.2"
|
"vitest": "^5.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import 'react-fontpicker-ts/dist/index.css';
|
|||||||
import '@gfazioli/mantine-border-animate/styles.css';
|
import '@gfazioli/mantine-border-animate/styles.css';
|
||||||
import '@gfazioli/mantine-parallax/styles.css';
|
import '@gfazioli/mantine-parallax/styles.css';
|
||||||
import '@gfazioli/mantine-text-animate/styles.css';
|
import '@gfazioli/mantine-text-animate/styles.css';
|
||||||
|
import '@gfazioli/mantine-reflection/styles.css';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { Homepage, Settings } from 'pages';
|
import { Homepage, Settings } from 'pages';
|
||||||
import FontPicker from 'react-fontpicker-ts';
|
import FontPicker from 'react-fontpicker-ts';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useEffect, useState } from 'react';
|
|||||||
import { FiChevronRight } from 'react-icons/fi';
|
import { FiChevronRight } from 'react-icons/fi';
|
||||||
import { textClasses } from 'styles';
|
import { textClasses } from 'styles';
|
||||||
import type { Theme, ThemePlaylist } from 'types';
|
import type { Theme, ThemePlaylist } from 'types';
|
||||||
import { ThemePlaylistCard } from './components/ThemePlaylistCard';
|
|
||||||
import { ThemePlaylistModal } from './components/ThemePlaylistModal';
|
import { ThemePlaylistModal } from './components/ThemePlaylistModal';
|
||||||
|
import { ThemePlaylistStack } from './components/ThemePlaylistStack';
|
||||||
|
|
||||||
export function ThemePlaylists() {
|
export function ThemePlaylists() {
|
||||||
const themesEndpoint = 'api/themes';
|
const themesEndpoint = 'api/themes';
|
||||||
@@ -134,7 +134,7 @@ export function ThemePlaylists() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<ThemePlaylistCard
|
<ThemePlaylistStack
|
||||||
cardPlaylist={{
|
cardPlaylist={{
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: 'All',
|
name: 'All',
|
||||||
@@ -165,7 +165,7 @@ export function ThemePlaylists() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<ThemePlaylistCard
|
<ThemePlaylistStack
|
||||||
cardPlaylist={{
|
cardPlaylist={{
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: 'Backgrounds',
|
name: 'Backgrounds',
|
||||||
@@ -196,7 +196,7 @@ export function ThemePlaylists() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<ThemePlaylistCard
|
<ThemePlaylistStack
|
||||||
cardPlaylist={{
|
cardPlaylist={{
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: 'No Backgrounds',
|
name: 'No Backgrounds',
|
||||||
@@ -227,7 +227,7 @@ export function ThemePlaylists() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<ThemePlaylistCard
|
<ThemePlaylistStack
|
||||||
cardPlaylist={{
|
cardPlaylist={{
|
||||||
id:
|
id:
|
||||||
themePlaylists?.find(
|
themePlaylists?.find(
|
||||||
@@ -289,7 +289,7 @@ export function ThemePlaylists() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<ThemePlaylistCard
|
<ThemePlaylistStack
|
||||||
cardPlaylist={cardPlaylist}
|
cardPlaylist={cardPlaylist}
|
||||||
handleOpen={handleOpen}
|
handleOpen={handleOpen}
|
||||||
allThemes={allThemeData}
|
allThemes={allThemeData}
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { ActionIcon, Button, Overlay, ScrollArea, Stack, Text } from '@mantine/core';
|
||||||
ActionIcon,
|
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Overlay,
|
|
||||||
ScrollArea,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { DeleteItemModal } from 'components';
|
import { DeleteItemModal } from 'components';
|
||||||
import { useCommonHooks } from 'hooks';
|
import { useCommonHooks } from 'hooks';
|
||||||
@@ -33,7 +25,7 @@ export function ThemePlaylistCard({
|
|||||||
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
|
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Group
|
<Stack
|
||||||
gap={0}
|
gap={0}
|
||||||
align='center'
|
align='center'
|
||||||
justify='center'
|
justify='center'
|
||||||
@@ -212,6 +204,6 @@ export function ThemePlaylistCard({
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
import { Parallax } from '@gfazioli/mantine-parallax';
|
||||||
|
import { Reflection } from '@gfazioli/mantine-reflection';
|
||||||
|
import { ActionIcon, Box, Group, Stack, Text } from '@mantine/core';
|
||||||
|
import { useDebouncedCallback, useDisclosure } from '@mantine/hooks';
|
||||||
|
import { DeleteItemModal } from 'components';
|
||||||
|
import { useCommonHooks } from 'hooks';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FiEdit, FiTrash } from 'react-icons/fi';
|
||||||
|
import { textClasses } from 'styles';
|
||||||
|
import type { Theme, ThemePlaylist } from 'types';
|
||||||
|
import { ThemeCard } from '../../Themes';
|
||||||
|
|
||||||
|
export function ThemePlaylistStack({
|
||||||
|
cardPlaylist,
|
||||||
|
handleOpen,
|
||||||
|
allThemes,
|
||||||
|
}: {
|
||||||
|
cardPlaylist: any;
|
||||||
|
handleOpen: (cardPlaylist: ThemePlaylist) => void;
|
||||||
|
allThemes: Theme[];
|
||||||
|
}) {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
const { setThemeName, setCurrentThemePlaylist, handleDelete, currentUser } =
|
||||||
|
useCommonHooks();
|
||||||
|
const [indexOffset, setIndexOffset] = useState(0);
|
||||||
|
const defaultLists = ['All', 'Backgrounds', 'No Backgrounds'];
|
||||||
|
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
|
||||||
|
const themeList = JSON.parse(
|
||||||
|
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingTheme = {
|
||||||
|
id: undefined,
|
||||||
|
name: 'default',
|
||||||
|
backgroundImage: 'none',
|
||||||
|
backgroundColor: '#EEEEEE',
|
||||||
|
searchColor: '#00000033',
|
||||||
|
primaryColor: '#222222',
|
||||||
|
accentColor: '#5c5c5c',
|
||||||
|
borderColor: '#5c5c5c',
|
||||||
|
randomEnabled: 1,
|
||||||
|
textShadowsEnabled: 0,
|
||||||
|
appShadowsEnabled: 0,
|
||||||
|
themeFont: currentUser.defaultFont,
|
||||||
|
};
|
||||||
|
|
||||||
|
const presentationProps = {
|
||||||
|
initialRotationX: 60,
|
||||||
|
initialRotationY: 0,
|
||||||
|
initialRotationZ: 0,
|
||||||
|
initialSkewX: -30,
|
||||||
|
initialSkewY: 30,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseWheel = useDebouncedCallback((e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.deltaY > 0) {
|
||||||
|
setIndexOffset((indexOffset % themeList.length) - 1);
|
||||||
|
} else {
|
||||||
|
setIndexOffset((indexOffset % themeList.length) + 1);
|
||||||
|
}
|
||||||
|
}, 75);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={0}
|
||||||
|
w={300}
|
||||||
|
pos={'relative'}
|
||||||
|
onMouseEnter={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
style={{
|
||||||
|
background: hover
|
||||||
|
? 'radial-gradient(var(--mantine-color-search-0), transparent)'
|
||||||
|
: undefined,
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: '100%',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentThemePlaylist(cardPlaylist.name);
|
||||||
|
setThemeName('random');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{modalOpened && (
|
||||||
|
<DeleteItemModal
|
||||||
|
modalOpened={modalOpened}
|
||||||
|
handleClose={closeModal}
|
||||||
|
handleDelete={() => handleDelete(cardPlaylist, 'api/themePlaylists')}
|
||||||
|
title='Delete Theme Playlist'
|
||||||
|
body={
|
||||||
|
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
|
||||||
|
<Text>Are you sure you want to delete this Theme Playlist?</Text>
|
||||||
|
<Text
|
||||||
|
className={textClasses.deleteItem}
|
||||||
|
classNames={textClasses}
|
||||||
|
>{`"${cardPlaylist.name}"`}</Text>
|
||||||
|
<Text c='red'>This action cannot be undone!</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{cardPlaylist?.name === 'Time of Day' ? (
|
||||||
|
<Reflection>
|
||||||
|
<Stack
|
||||||
|
w='100%'
|
||||||
|
h='200px'
|
||||||
|
justify='center'
|
||||||
|
pos='relative'
|
||||||
|
gap={16}
|
||||||
|
style={{ overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<Parallax
|
||||||
|
{...presentationProps}
|
||||||
|
style={{ position: 'absolute', top: -60, zIndex: 3 }}
|
||||||
|
>
|
||||||
|
<ThemeCard
|
||||||
|
cardTheme={
|
||||||
|
allThemes.find((theme: Theme) => theme.id === Number(themeList[0])) ||
|
||||||
|
missingTheme
|
||||||
|
}
|
||||||
|
allThemes={allThemes}
|
||||||
|
themePlaylists={[]}
|
||||||
|
setSelectedTheme={() => {}}
|
||||||
|
openThemeModal={() => {}}
|
||||||
|
enabled={false}
|
||||||
|
management={false}
|
||||||
|
/>
|
||||||
|
</Parallax>
|
||||||
|
<Parallax
|
||||||
|
{...presentationProps}
|
||||||
|
style={{ position: 'absolute', top: -60, zIndex: 2 }}
|
||||||
|
>
|
||||||
|
<ThemeCard
|
||||||
|
cardTheme={
|
||||||
|
allThemes.find((theme: Theme) => theme.id === Number(themeList[1])) ||
|
||||||
|
missingTheme
|
||||||
|
}
|
||||||
|
allThemes={allThemes}
|
||||||
|
themePlaylists={[]}
|
||||||
|
setSelectedTheme={() => {}}
|
||||||
|
openThemeModal={() => {}}
|
||||||
|
enabled={false}
|
||||||
|
management={false}
|
||||||
|
/>
|
||||||
|
</Parallax>
|
||||||
|
<Parallax
|
||||||
|
{...presentationProps}
|
||||||
|
style={{ position: 'absolute', top: -60, zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<ThemeCard
|
||||||
|
cardTheme={
|
||||||
|
allThemes.find((theme: Theme) => theme.id === Number(themeList[2])) ||
|
||||||
|
missingTheme
|
||||||
|
}
|
||||||
|
allThemes={allThemes}
|
||||||
|
themePlaylists={[]}
|
||||||
|
setSelectedTheme={() => {}}
|
||||||
|
openThemeModal={() => {}}
|
||||||
|
enabled={false}
|
||||||
|
management={false}
|
||||||
|
/>
|
||||||
|
</Parallax>
|
||||||
|
<Parallax
|
||||||
|
{...presentationProps}
|
||||||
|
style={{ position: 'absolute', top: -60, zIndex: 0 }}
|
||||||
|
>
|
||||||
|
<ThemeCard
|
||||||
|
cardTheme={
|
||||||
|
allThemes.find((theme: Theme) => theme.id === Number(themeList[3])) ||
|
||||||
|
missingTheme
|
||||||
|
}
|
||||||
|
allThemes={allThemes}
|
||||||
|
themePlaylists={[]}
|
||||||
|
setSelectedTheme={() => {}}
|
||||||
|
openThemeModal={() => {}}
|
||||||
|
enabled={false}
|
||||||
|
management={false}
|
||||||
|
/>
|
||||||
|
</Parallax>
|
||||||
|
</Stack>
|
||||||
|
</Reflection>
|
||||||
|
) : (
|
||||||
|
<Reflection>
|
||||||
|
<Stack
|
||||||
|
w='100%'
|
||||||
|
h='200px'
|
||||||
|
justify='start'
|
||||||
|
pos='relative'
|
||||||
|
gap={0}
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'all 0.5s ease-in-out',
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
}}
|
||||||
|
onWheel={handleMouseWheel}
|
||||||
|
>
|
||||||
|
{themeList
|
||||||
|
.sort((a: number, b: number) => (a > b ? 1 : -1))
|
||||||
|
.map((item: number, index: number) => {
|
||||||
|
let spot = index + indexOffset;
|
||||||
|
if (spot <= -1) {
|
||||||
|
spot = index + indexOffset + themeList.length;
|
||||||
|
} else if (spot >= themeList.length) {
|
||||||
|
spot = index + indexOffset - themeList.length;
|
||||||
|
} else {
|
||||||
|
spot = index + indexOffset;
|
||||||
|
}
|
||||||
|
const theme: any = allThemes.find((theme: Theme) => theme.id === item);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={theme.id}
|
||||||
|
style={{
|
||||||
|
transition: 'all 0.3s ease-in-out',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16 * (spot + 1),
|
||||||
|
zIndex: themeList.length - spot,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Parallax {...presentationProps}>
|
||||||
|
<ThemeCard
|
||||||
|
key={theme.id}
|
||||||
|
cardTheme={theme}
|
||||||
|
allThemes={allThemes}
|
||||||
|
themePlaylists={[]}
|
||||||
|
setSelectedTheme={() => {}}
|
||||||
|
openThemeModal={() => {}}
|
||||||
|
enabled={false}
|
||||||
|
management={false}
|
||||||
|
/>
|
||||||
|
</Parallax>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Reflection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group
|
||||||
|
w={'100%'}
|
||||||
|
pos={'absolute'}
|
||||||
|
justify='space-between'
|
||||||
|
bottom={0}
|
||||||
|
style={{ zIndex: 1000 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
paddingRight: '.25rem',
|
||||||
|
paddingLeft: '.25rem',
|
||||||
|
backgroundColor: 'var(--mantine-color-border-0)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'var(--mantine-color-primary-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardPlaylist.name}
|
||||||
|
</Text>
|
||||||
|
<Group gap={0}>
|
||||||
|
{!defaultLists.includes(cardPlaylist.name) && (
|
||||||
|
<ActionIcon
|
||||||
|
style={{
|
||||||
|
paddingRight: '.25rem',
|
||||||
|
paddingLeft: '.25rem',
|
||||||
|
backgroundColor: 'var(--mantine-color-border-0)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'var(--mantine-color-background-0)',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
handleOpen(cardPlaylist);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
{cardPlaylist.default === 0 && (
|
||||||
|
<ActionIcon
|
||||||
|
color='red'
|
||||||
|
style={{
|
||||||
|
paddingRight: '.25rem',
|
||||||
|
paddingLeft: '.25rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
openModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiTrash color='black' />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user