mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
Adds user avatar to header and updates create model form
This commit is contained in:
+3
-7
@@ -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 +0,0 @@
|
||||
module.exports = require('eslint-config-mantine/.prettierrc.js');
|
||||
+2
-1
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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: {} };
|
||||
};
|
||||
|
||||
@@ -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,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,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user