feat: user avatars

This commit is contained in:
diced 2022-08-16 14:50:59 -07:00
parent 8f835eec4e
commit d41f6058f7
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
10 changed files with 115 additions and 14 deletions

View file

@ -1,3 +1,6 @@
/**
* @type {import('next').NextConfig}
**/
module.exports = {
async redirects() {
return [

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "avatar" TEXT;

View file

@ -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

View file

@ -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',

View file

@ -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>

View file

@ -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>

View file

@ -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,
},
});

View file

@ -8,6 +8,7 @@ export interface User {
embedSiteName: string;
systemTheme: string;
domains: string[];
avatar?: string;
}
const initialState: User = null;

View file

@ -23,7 +23,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
username: req.body.username,
},
});
if (existing && user.username !== req.body.username) {
if (existing && user.username !== req.body.username) {
return res.forbid('Username is already taken');
}
await prisma.user.update({
@ -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 },
@ -57,7 +62,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
where: { id: user.id },
data: { domains: [] },
});
const invalidDomains = [];
for (const domain of req.body.domains) {
@ -98,6 +103,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
token: true,
username: true,
domains: true,
avatar: true,
},
});
@ -111,4 +117,4 @@ async function handler(req: NextApiReq, res: NextApiRes) {
}
}
export default withZipline(handler);
export default withZipline(handler);

View file

@ -10,7 +10,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
where: { code },
});
if (!invite) return res.bad('invalid invite code');
const user = await prisma.user.findFirst({
where: { username },
});
@ -25,7 +25,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
if (req.method === 'DELETE') {
if (req.body.id === user.id) return res.forbid('you can\'t delete your own account');
const deleteUser = await prisma.user.findFirst({
where: {
id: req.body.id,
@ -70,6 +70,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
embedColor: true,
embedTitle: true,
systemTheme: true,
avatar: true,
},
});
return res.json(users);