diff --git a/.eslintrc.json b/.eslintrc.json index fa75147..80045cf 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,6 +15,7 @@ "plugin:react/jsx-runtime" ], "rules": { + "no-nested-ternary": "off", "react/jsx-props-no-spreading": "off", "no-underscore-dangle": "off", "import/extensions": [ diff --git a/package.json b/package.json index 009123d..0635e49 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@mantine/nprogress": "^5.5.4", "@mantine/spotlight": "^5.5.4", "@prisma/client": "4.4.0", + "@tabler/icons": "^1.101.0", "@tanstack/react-query": "^4.9.0", "@trpc/client": "^10.0.0-proxy-beta.13", "@trpc/next": "^10.0.0-proxy-beta.13", diff --git a/public/cover-not-found.jpg b/public/cover-not-found.jpg new file mode 100644 index 0000000..378da68 Binary files /dev/null and b/public/cover-not-found.jpg differ diff --git a/src/components/addLibrary.tsx b/src/components/addLibrary.tsx index 73b6a4d..bb15a14 100644 --- a/src/components/addLibrary.tsx +++ b/src/components/addLibrary.tsx @@ -2,9 +2,9 @@ import { Box, Button, Code, LoadingOverlay, Text, TextInput } from '@mantine/cor import { useForm } from '@mantine/form'; import { useModals } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; +import { IconCheck, IconX } from '@tabler/icons'; import { useState } from 'react'; -import { IoCloseCircle } from 'react-icons/io5'; -import { MdCheckCircleOutline, MdOutlineCreateNewFolder } from 'react-icons/md'; +import { MdOutlineCreateNewFolder } from 'react-icons/md'; import { trpc } from '../utils/trpc'; function Form({ onClose }: { onClose: () => void }) { @@ -35,7 +35,7 @@ function Form({ onClose }: { onClose: () => void }) { }); } catch (err) { showNotification({ - icon: , + icon: , color: 'red', autoClose: true, title: 'Library', @@ -55,7 +55,7 @@ function Form({ onClose }: { onClose: () => void }) { onClose(); setVisible((v) => !v); showNotification({ - icon: , + icon: , color: 'teal', autoClose: true, title: 'Library', diff --git a/src/components/mangaCard.tsx b/src/components/mangaCard.tsx index b12fa0e..160cca7 100644 --- a/src/components/mangaCard.tsx +++ b/src/components/mangaCard.tsx @@ -1,4 +1,5 @@ import { Badge, Button, createStyles, Paper, Title } from '@mantine/core'; +import { IconExternalLink } from '@tabler/icons'; const useStyles = createStyles((theme) => ({ card: { @@ -35,7 +36,7 @@ const useStyles = createStyles((theme) => ({ interface ArticleCardImageProps { image: string; title: string; - category: string; + category?: string; } export function MangaCard({ image, title, category }: ArticleCardImageProps) { @@ -50,16 +51,22 @@ export function MangaCard({ image, title, category }: ArticleCardImageProps) { className={classes.card} >
- - {category} - + {category && ( + + {category} + + )} {title}
- ); } + +MangaCard.defaultProps = { + category: '', +}; diff --git a/src/components/mangaSearchResult.tsx b/src/components/mangaSearchResult.tsx new file mode 100644 index 0000000..918c3b6 --- /dev/null +++ b/src/components/mangaSearchResult.tsx @@ -0,0 +1,142 @@ +import { createStyles, Image, SimpleGrid, Text, UnstyledButton } from '@mantine/core'; +import { useUncontrolled } from '@mantine/hooks'; +import { useState } from 'react'; + +const useStyles = createStyles((theme, { checked, disabled }: { checked: boolean; disabled: boolean }) => ({ + button: { + display: 'flex', + alignItems: 'center', + width: '100%', + transition: 'background-color 150ms ease, border-color 150ms ease', + border: `1px solid ${ + checked + ? theme.fn.variant({ variant: 'outline', color: theme.primaryColor }).border + : theme.colorScheme === 'dark' + ? theme.colors.dark[8] + : theme.colors.gray[3] + }`, + borderRadius: theme.radius.sm, + padding: theme.spacing.sm, + backgroundColor: checked + ? theme.fn.variant({ variant: 'light', color: theme.primaryColor }).background + : disabled + ? theme.colors.gray[3] + : theme.white, + }, + + body: { + flex: 1, + marginLeft: theme.spacing.md, + }, +})); + +interface ImageCheckboxProps { + checked?: boolean; + defaultChecked?: boolean; + onChange?(checked: boolean): void; + title: string; + description: string; + image: string; +} + +export function ImageCheckbox({ + checked, + defaultChecked, + onChange, + title, + description, + className, + disabled, + image, + ...others +}: ImageCheckboxProps & Omit, keyof ImageCheckboxProps>) { + const [value, handleChange] = useUncontrolled({ + value: checked, + defaultValue: defaultChecked, + finalValue: false, + onChange, + }); + + const { classes, cx } = useStyles({ checked: value, disabled: disabled || false }); + + return ( + { + if (!disabled) { + handleChange(!value); + } + }} + className={cx(classes.button, className)} + > + {title}} + src={image} + width={42} + height={64} + /> + +
+ + {description} + + + {title} + +
+
+ ); +} + +ImageCheckbox.defaultProps = { + checked: undefined, + defaultChecked: undefined, + onChange: () => {}, +}; + +type IMangaSearchResult = { + status: string; + title: string; + order: number; + cover: string; +}; + +export function MangaSearchResult({ + items, + onSelect, +}: { + items: IMangaSearchResult[]; + onSelect: (selected: IMangaSearchResult | undefined) => void; +}) { + const [selected, setSelected] = useState(); + + return ( + + {items.map((m) => ( + { + if (checked) { + setSelected(m); + onSelect(m); + } else { + setSelected(undefined); + onSelect(undefined); + } + }} + /> + ))} + + ); +} diff --git a/src/components/newMangaCard.tsx b/src/components/newMangaCard.tsx index 159f492..443f9df 100644 --- a/src/components/newMangaCard.tsx +++ b/src/components/newMangaCard.tsx @@ -1,4 +1,32 @@ -import { Button, createStyles, Paper } from '@mantine/core'; +import { + ActionIcon, + Badge, + Box, + Button, + Code, + createStyles, + Divider, + Grid, + Group, + Image, + LoadingOverlay, + Paper, + Select, + Stepper, + Text, + TextInput, + Title, + Tooltip, +} from '@mantine/core'; +import { useForm, UseFormReturnType, zodResolver } from '@mantine/form'; +import { getHotkeyHandler } from '@mantine/hooks'; +import { useModals } from '@mantine/modals'; +import { showNotification } from '@mantine/notifications'; +import { IconArrowRight, IconBook, IconCheck, IconSearch, IconX } from '@tabler/icons'; +import { useState } from 'react'; +import { z } from 'zod'; +import { trpc } from '../utils/trpc'; +import { MangaSearchResult } from './mangaSearchResult'; const useStyles = createStyles((theme) => ({ card: { @@ -18,11 +46,414 @@ const useStyles = createStyles((theme) => ({ boxShadow: theme.shadows.md, }, }, + form: { + display: 'flex', + flexDirection: 'column', + minHeight: 300, + padding: theme.spacing.xs, + }, + stepper: { + flexGrow: 1, + }, + stepBody: { + marginTop: 30, + marginBottom: 30, + }, + buttonGroup: { + position: 'fixed', + bottom: '19px', + right: '55px', + width: 'calc(100% - 55px)', + height: '50px', + background: 'white', + }, + placeHolder: { + alignItems: 'start !important', + justifyContent: 'flex-start !important', + }, })); -export function NewMangaCard() { +const schema = z.object({ + source: z.string().min(1, { message: 'You must select a source' }), + query: z.string().min(1, { message: 'Cannot be empty' }), + mangaOrder: z.number().gte(0, { message: 'Please select a manga' }), + mangaTitle: z.string().min(1, { message: 'Please select a manga' }), +}); + +type FormType = z.TypeOf; + +function SourceStep({ form }: { form: UseFormReturnType }) { + const query = trpc.manga.sources.useQuery(undefined, { + staleTime: Infinity, + }); + + if (query.isLoading) { + return ; + } + + const selectData = query.data?.map((s) => ({ + value: s, + label: s, + })); + + return ( + +