Adds user avatar to header and updates create model form

This commit is contained in:
Manuel E. Urena
2022-10-14 13:28:40 -04:00
parent ca7263fea1
commit d4d074435e
10 changed files with 246 additions and 70 deletions
+3 -7
View File
@@ -9,7 +9,6 @@ module.exports = {
'import',
],
extends: [
'mantine',
'next/core-web-vitals',
'prettier',
'plugin:@next/next/recommended',
@@ -25,15 +24,12 @@ module.exports = {
// prettier overrides
'prettier/prettier': ['error', {
printWidth: 100,
endOfLine: 'auto'
endOfLine: 'auto',
singleQuote: true,
trailingComma: "es5",
}],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
'no-restricted-syntax': [
'error',
'LabeledStatement',
'WithStatement',
],
},
settings: {
'import/parsers': {
-1
View File
@@ -1 +0,0 @@
module.exports = require('eslint-config-mantine/.prettierrc.js');
+2 -1
View File
@@ -11,7 +11,8 @@
"typecheck": "tsc --noEmit",
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"db:ui": "prisma studio"
"db:ui": "prisma studio",
"db:migrate": "prisma migrate dev"
},
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
+167 -18
View File
@@ -1,44 +1,193 @@
import { Anchor, Autocomplete, Button, Group, Header, Title } from '@mantine/core';
import {
Anchor,
Autocomplete,
Avatar,
Burger,
Button,
createStyles,
Group,
Header,
Menu,
Title,
UnstyledButton,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { NextLink } from '@mantine/next';
import { IconSearch } from '@tabler/icons';
import {
IconChevronDown,
IconFile,
IconHeart,
IconLogout,
IconSearch,
IconSettings,
IconStar,
} from '@tabler/icons';
import { signOut, useSession } from 'next-auth/react';
import Link from 'next/link';
import { useState } from 'react';
import { ColorSchemeToggle } from '~/components/ColorSchemeToggle/ColorSchemeToggle';
const useStyles = createStyles((theme) => ({
inner: {
height: 70,
justifyContent: 'space-between',
},
burger: {
[theme.fn.largerThan('xs')]: {
display: 'none',
},
},
search: {
[theme.fn.smallerThan('xs')]: {
display: 'none',
},
},
links: {
[theme.fn.smallerThan('md')]: {
display: 'none',
},
},
link: {
display: 'block',
lineHeight: 1,
padding: '8px 12px',
borderRadius: theme.radius.sm,
textDecoration: 'none',
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.gray[7],
fontSize: theme.fontSizes.sm,
fontWeight: 500,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
},
},
user: {
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
padding: `${theme.spacing.xs}px ${theme.spacing.sm}px`,
borderRadius: theme.radius.sm,
transition: 'background-color 100ms ease',
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
},
[theme.fn.smallerThan('xs')]: {
display: 'none',
},
},
userActive: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
},
}));
export function AppHeader({ links }: Props) {
const { data: session } = useSession();
const [burgerOpened, { toggle: toggleBurger }] = useDisclosure(false);
const [userMenuOpened, setUserMenuOpened] = useState(false);
const { classes, cx, theme } = useStyles();
return (
<Header p="sm" height={70}>
<Group align="center" sx={{ justifyContent: 'space-between' }}>
<Group>
<Header px="md" height={70}>
<Group align="center" className={classes.inner}>
<Group spacing="sm">
<Burger
className={classes.burger}
opened={burgerOpened}
onClick={toggleBurger}
size="sm"
/>
<Link href="/" passHref>
<Anchor variant="text">
<Title>MS</Title>
</Anchor>
</Link>
<Autocomplete
className={classes.search}
placeholder="Search"
icon={<IconSearch size={16} stroke={1.5} />}
data={['React', 'Angular', 'Vue', 'Next.js', 'Riot.js', 'Svelte', 'Blitz.js']}
/>
</Group>
<Group>
<Group ml={50} spacing={5}>
<Group spacing="sm">
<Group spacing="sm">
{links?.map((link) => (
<a key={link.label} href={link.url} onClick={(event) => event.preventDefault()}>
{link.label}
</a>
<Link key={link.label} href={link.url} passHref>
<Anchor className={classes.link} variant="text">
{link.label}
</Anchor>
</Link>
))}
</Group>
<ColorSchemeToggle />
{session ? (
<Button onClick={() => signOut({ callbackUrl: '/' })}>Sign Out</Button>
) : (
<Button component={NextLink} href="/login">
Sign In
</Button>
)}
<Group spacing="xs">
{session ? (
<Button component={NextLink} href="/models/create">
Upload a model
</Button>
) : null}
<ColorSchemeToggle />
{session ? (
<Menu
position="bottom-end"
transition="pop-top-right"
onClose={() => setUserMenuOpened(false)}
onOpen={() => setUserMenuOpened(true)}
>
<Menu.Target>
<UnstyledButton
className={cx(classes.user, { [classes.userActive]: userMenuOpened })}
>
<Group spacing={7}>
<Avatar
src={session?.user?.image}
alt={session?.user?.name ?? 'User avatar'}
radius="xl"
size={20}
/>
<IconChevronDown size={12} stroke={1.5} />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={<IconHeart size={14} color={theme.colors.red[6]} stroke={1.5} />}
>
Liked models
</Menu.Item>
<Menu.Item
icon={<IconStar size={14} color={theme.colors.yellow[6]} stroke={1.5} />}
>
Saved models
</Menu.Item>
<Menu.Item
icon={<IconFile size={14} color={theme.colors.blue[6]} stroke={1.5} />}
>
Your models
</Menu.Item>
<Menu.Label>Settings</Menu.Label>
<Menu.Item icon={<IconSettings size={14} stroke={1.5} />}>
Account settings
</Menu.Item>
<Menu.Item
icon={<IconLogout size={14} color={theme.colors.red[9]} stroke={1.5} />}
onClick={() => signOut()}
>
Logout
</Menu.Item>
</Menu.Dropdown>
</Menu>
) : (
<Button component={NextLink} href="/login" variant="outline">
Sign In
</Button>
)}
</Group>
</Group>
</Group>
</Header>
+8 -1
View File
@@ -6,7 +6,14 @@ export function AppLayout({ children, showNavbar }: Props) {
return (
<AppShell
padding="md"
header={<AppHeader />}
header={
<AppHeader
links={[
{ label: 'Models', url: '#' },
{ label: 'Link 2', url: '#' },
]}
/>
}
navbar={showNavbar ? <SideNavigation /> : undefined}
>
{children}
-17
View File
@@ -1,17 +0,0 @@
import { Group, Paper, PaperProps, Text } from '@mantine/core';
import { DiscordButton, GitHubButton } from '~/components/SocialButtons/SocialButtons';
export function AuthForm(props: PaperProps) {
return (
<Paper radius="md" p="xl" withBorder {...props}>
<Text size="lg" weight={500}>
Welcome to Model Share, sign in with
</Text>
<Group grow mb="md" mt="md">
<GitHubButton radius="xl">GitHub</GitHubButton>
<DiscordButton radius="xl">Discord</DiscordButton>
</Group>
</Paper>
);
}
@@ -8,12 +8,9 @@ export function ColorSchemeToggle() {
<Group position="center">
<ActionIcon
onClick={() => toggleColorScheme()}
size="xl"
sx={(theme) => ({
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
color: theme.colorScheme === 'dark' ? theme.colors.yellow[4] : theme.colors.blue[6],
})}
size="lg"
variant="outline"
color={colorScheme === 'dark' ? 'yellow' : 'blue'}
>
{colorScheme === 'dark' ? (
<IconSun size={20} stroke={1.5} />
+54 -10
View File
@@ -1,47 +1,91 @@
import { Button, Container, Select, Stack, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { Button, Container, MultiSelect, Select, Stack, Textarea, TextInput } from '@mantine/core';
import { FormErrors, useForm, zodResolver } from '@mantine/form';
import { GetServerSideProps } from 'next';
import { useState } from 'react';
import { z } from 'zod';
import { getServerAuthSession } from '~/server/common/get-server-auth-session';
import { modelSchema } from '~/server/common/validation/model';
type CreateModelProps = Partial<z.infer<typeof modelSchema>>;
type MultiSelectCreatable = Array<{ value: string; label: string }>;
const Create = () => {
export default function Create() {
const form = useForm<CreateModelProps>({
validate: zodResolver(modelSchema.passthrough()),
initialValues: {
name: '',
description: '',
trainedWords: [],
// type: 'Checkpoint',
type: 'Checkpoint',
tags: [],
nsfw: false,
modelVersions: [],
},
});
const [trainedWords, setTrainedWords] = useState<MultiSelectCreatable>([]);
const handleSubmit = (data: CreateModelProps) => {
console.log({ data });
};
const handleError = (error: any) => {};
const handleError = (error: FormErrors) => {
console.error(error);
};
return (
<Container>
<form onSubmit={form.onSubmit(handleSubmit, handleError)}>
<form onSubmit={form.onSubmit(handleSubmit, handleError)} noValidate>
<Stack>
<TextInput label="Name" placeholder="Name" id="name" />
<TextInput {...form.getInputProps('name')} label="Name" placeholder="Name" required />
<Textarea
{...form.getInputProps('description')}
label="About your model"
maxLength={240}
/>
<Select
{...form.getInputProps('type')}
label="Type"
placeholder="Type"
{...form.getInputProps('type')}
data={['Checkpoint', 'TextualInversion', 'Hypernetwork']}
required
/>
<MultiSelect
{...form.getInputProps('trainedWords')}
label="Trained Words"
placeholder="e.g.: Master Chief"
description="Please input the words you have trained your model with"
data={trainedWords}
getCreateLabel={(query) => `+ Create ${query}`}
onCreate={(query) => {
const item = { value: query, label: query };
setTrainedWords((current) => [...current, item]);
return item;
}}
clearButtonLabel="Clear trained words"
creatable
clearable
searchable
required
/>
<Button type="submit">Submit</Button>
</Stack>
</form>
</Container>
);
};
}
export default Create;
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
return { props: {} };
};
+4 -4
View File
@@ -22,11 +22,11 @@ export const modelVersionSchema = z.object({
});
export const modelSchema = z.object({
name: z.string(),
name: z.string().min(1, 'Name cannot be empty.'),
description: z.string().optional(),
type: z.nativeEnum(ModelType),
trainedWords: z.string().array(),
tags: z.string().array(),
trainedWords: z.array(z.string()).min(1, 'At least one trained word is required.'),
tags: z.array(z.string()),
nsfw: z.boolean(),
modelVersions: z.array(modelVersionSchema).min(1, 'At least one model version is required'),
modelVersions: z.array(modelVersionSchema).min(1, 'At least one model version is required.'),
});
+5 -5
View File
@@ -5,10 +5,10 @@ import { handleDbError } from '~/server/services/errorHandling';
import { protectedProcedure, publicProcedure, router } from '../trpc';
const baseQuerySelect = {
ModelVersions: { select: { url: true, description: true } },
Reviews: { select: { text: true, rating: true, user: true } },
TagsOnModels: { select: { tag: true } },
ImagesOnModels: { select: { image: true } },
modelVersions: { select: { url: true, description: true } },
reviews: { select: { text: true, rating: true, user: true } },
tagsOnModels: { select: { tag: true } },
imagesOnModels: { select: { image: true } },
};
export const modelRouter = router({
@@ -78,7 +78,7 @@ export const modelRouter = router({
const createdModels = await ctx.prisma.model.create({
data: {
...data,
ModelVersions: {
modelVersions: {
create: modelVersions,
},
},