feat: user avatars
This commit is contained in:
parent
8f835eec4e
commit
d41f6058f7
10 changed files with 115 additions and 14 deletions
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
**/
|
||||
module.exports = {
|
||||
async redirects() {
|
||||
return [
|
||||
|
|
2
prisma/migrations/20220816212407_avatar/migration.sql
Normal file
2
prisma/migrations/20220816212407_avatar/migration.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "avatar" TEXT;
|
|
@ -11,6 +11,7 @@ model User {
|
|||
id Int @id @default(autoincrement())
|
||||
username String
|
||||
password String
|
||||
avatar String?
|
||||
token String
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("system")
|
||||
|
@ -79,11 +80,11 @@ model Stats {
|
|||
}
|
||||
|
||||
model Invite {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
created_at DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
created_at DateTime @default(now())
|
||||
expires_at DateTime?
|
||||
used Boolean @default(false)
|
||||
used Boolean @default(false)
|
||||
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
createdById Int
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group } from '@mantine/core';
|
||||
import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group, Image } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
|
@ -111,8 +111,10 @@ const admin_items = [
|
|||
export default function Layout({ children, user, title }) {
|
||||
const [token, setToken] = useState(user?.token);
|
||||
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
|
||||
const [avatar, setAvatar] = useState(user.avatar ?? null);
|
||||
const [opened, setOpened] = useState(false); // navigation open
|
||||
const [open, setOpen] = useState(false); // manage acc dropdown
|
||||
|
||||
const router = useRouter();
|
||||
const dispatch = useStoreDispatch();
|
||||
const theme = useMantineTheme();
|
||||
|
@ -258,7 +260,7 @@ export default function Layout({ children, user, title }) {
|
|||
>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
leftIcon={<SettingsIcon />}
|
||||
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
sx={t => ({
|
||||
backgroundColor: '#00000000',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Box, Button, Card, ColorInput, Group, MultiSelect, Space, Text, TextInput, PasswordInput, Title, Tooltip } from '@mantine/core';
|
||||
import { Box, Button, Card, ColorInput, Group, MultiSelect, Space, Text, TextInput, PasswordInput, Title, Tooltip, FileInput, Image } from '@mantine/core';
|
||||
import { randomId, useInterval } from '@mantine/hooks';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { CrossIcon, DeleteIcon } from 'components/icons';
|
||||
import { CrossIcon, DeleteIcon, SettingsIcon } from 'components/icons';
|
||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||
import Link from 'components/Link';
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
|
@ -25,6 +25,63 @@ export default function Manage() {
|
|||
|
||||
const [exports, setExports] = useState([]);
|
||||
const [domains, setDomains] = useState(user.domains ?? []);
|
||||
const [file, setFile] = useState<File>(null);
|
||||
const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null);
|
||||
|
||||
const getDataURL = (f: File): Promise<string> => {
|
||||
return new Promise((res, rej) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', () => {
|
||||
res(reader.result as string);
|
||||
});
|
||||
|
||||
reader.addEventListener('error', () => {
|
||||
rej(reader.error);
|
||||
});
|
||||
|
||||
reader.readAsDataURL(f);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAvatarChange = async (file: File) => {
|
||||
setFile(file);
|
||||
|
||||
setFileDataURL(await getDataURL(file));
|
||||
};
|
||||
|
||||
const saveAvatar = async () => {
|
||||
const dataURL = await getDataURL(file);
|
||||
|
||||
showNotification({
|
||||
id: 'update-user',
|
||||
title: 'Updating user...',
|
||||
message: '',
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
});
|
||||
|
||||
const newUser = await useFetch('/api/user', 'PATCH', {
|
||||
avatar: dataURL,
|
||||
});
|
||||
|
||||
if (newUser.error) {
|
||||
updateNotification({
|
||||
id: 'update-user',
|
||||
title: 'Couldn\'t save user',
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
dispatch(updateUser(newUser));
|
||||
updateNotification({
|
||||
id: 'update-user',
|
||||
title: 'Saved User',
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => {
|
||||
const config = {
|
||||
|
@ -223,6 +280,32 @@ export default function Manage() {
|
|||
</Group>
|
||||
</form>
|
||||
|
||||
<Box mb='md'>
|
||||
<Title>Avatar</Title>
|
||||
<FileInput id='file' description='Add a custom avatar or leave blank for none' accept='image/png,image/jpeg,image/gif' value={file} onChange={handleAvatarChange} />
|
||||
<Card mt='md'>
|
||||
<Text>Preview:</Text>
|
||||
<Button
|
||||
leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />}
|
||||
sx={t => ({
|
||||
backgroundColor: '#00000000',
|
||||
'&:hover': {
|
||||
backgroundColor: t.other.hover,
|
||||
},
|
||||
})}
|
||||
size='xl'
|
||||
p='sm'
|
||||
>
|
||||
{user.username}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Button onClick={() => { setFile(null); setFileDataURL(null); }}>Reset</Button>
|
||||
<Button onClick={saveAvatar} >Save Avatar</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Box mb='md'>
|
||||
<Title>Manage Data</Title>
|
||||
<MutedText size='md'>Delete, or export your data into a zip file.</MutedText>
|
||||
|
|
|
@ -157,7 +157,7 @@ export default function Users() {
|
|||
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
|
||||
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'} src={user.avatar ?? null}>{user.username[0]}</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>{user.username}</Title>
|
||||
<MutedText size='sm'>ID: {user.id}</MutedText>
|
||||
|
|
|
@ -26,6 +26,7 @@ export type NextApiReq = NextApiRequest & {
|
|||
id: number;
|
||||
password: string;
|
||||
domains: string[];
|
||||
avatar?: string;
|
||||
} | null | void>;
|
||||
getCookie: (name: string) => string | null;
|
||||
cleanCookie: (name: string) => void;
|
||||
|
@ -114,6 +115,7 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
|||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface User {
|
|||
embedSiteName: string;
|
||||
systemTheme: string;
|
||||
domains: string[];
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
const initialState: User = null;
|
||||
|
|
|
@ -32,6 +32,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
});
|
||||
}
|
||||
|
||||
if (req.body.avatar) await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { avatar: req.body.avatar },
|
||||
});
|
||||
|
||||
if (req.body.embedTitle) await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { embedTitle: req.body.embedTitle },
|
||||
|
@ -98,6 +103,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
embedColor: true,
|
||||
embedTitle: true,
|
||||
systemTheme: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
return res.json(users);
|
||||
|
|
Loading…
Reference in a new issue