From 93154a5a15065e4eec1cabff141742c4be8a6dd1 Mon Sep 17 00:00:00 2001 From: Alperen Elhan Date: Fri, 14 Oct 2022 00:35:58 +0300 Subject: [PATCH] refactor: cleanup add manga code --- src/components/addManga/form.tsx | 163 ++++++ src/components/addManga/index.tsx | 55 ++ .../{ => addManga}/mangaSearchResult.tsx | 0 .../addManga/steps/downloadStep.tsx | 48 ++ src/components/addManga/steps/index.tsx | 80 +++ src/components/addManga/steps/reviewStep.tsx | 128 +++++ src/components/addManga/steps/searchStep.tsx | 70 +++ src/components/addManga/steps/sourceStep.tsx | 30 + src/components/newMangaCard.tsx | 533 ------------------ src/pages/index.tsx | 4 +- 10 files changed, 576 insertions(+), 535 deletions(-) create mode 100644 src/components/addManga/form.tsx create mode 100644 src/components/addManga/index.tsx rename src/components/{ => addManga}/mangaSearchResult.tsx (100%) create mode 100644 src/components/addManga/steps/downloadStep.tsx create mode 100644 src/components/addManga/steps/index.tsx create mode 100644 src/components/addManga/steps/reviewStep.tsx create mode 100644 src/components/addManga/steps/searchStep.tsx create mode 100644 src/components/addManga/steps/sourceStep.tsx delete mode 100644 src/components/newMangaCard.tsx diff --git a/src/components/addManga/form.tsx b/src/components/addManga/form.tsx new file mode 100644 index 0000000..89d1cbd --- /dev/null +++ b/src/components/addManga/form.tsx @@ -0,0 +1,163 @@ +import { Button, Code, createStyles, Group, LoadingOverlay, Text } from '@mantine/core'; +import { useForm, zodResolver } from '@mantine/form'; +import { showNotification } from '@mantine/notifications'; +import { IconCheck, IconX } from '@tabler/icons'; +import { useState } from 'react'; +import { z } from 'zod'; +import { trpc } from '../../utils/trpc'; +import AddMangaSteps from './steps'; + +const useStyles = createStyles((theme) => ({ + form: { + display: 'flex', + flexDirection: 'column', + minHeight: 300, + padding: theme.spacing.xs, + }, + buttonGroup: { + position: 'fixed', + bottom: '19px', + right: '55px', + width: 'calc(100% - 55px)', + height: '50px', + background: 'white', + }, +})); + +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' }), + interval: z.string().min(1, { message: 'Please select an interval' }), +}); + +export type FormType = z.TypeOf; + +export function AddMangaForm({ onClose }: { onClose: () => void }) { + const [active, setActive] = useState(0); + const [visible, setVisible] = useState(false); + const { classes } = useStyles(); + + const mutation = trpc.manga.add.useMutation(); + + const form = useForm({ + validateInputOnBlur: ['source', 'query', 'interval'], + initialValues: { + source: '', + query: '', + mangaOrder: -1, + mangaTitle: '', + interval: '', + }, + validate: zodResolver(schema), + }); + + const nextStep = () => { + if (active === 0) { + form.validateField('source'); + if (!form.isValid('source')) { + return; + } + } + if (active === 1) { + form.validateField('mangaTitle'); + form.validateField('mangaOrder'); + if (!form.isValid('mangaOrder') || !form.isValid('mangaTitle')) { + return; + } + } + if (active === 2) { + form.validateField('interval'); + if (!form.isValid('interval')) { + return; + } + } + form.clearErrors(); + setActive((current) => (current < 3 ? current + 1 : current)); + }; + + const prevStep = () => { + if (active === 0) { + setVisible(false); + onClose(); + form.reset(); + } + if (active === 1) { + form.setFieldValue('query', ''); + form.setFieldValue('source', ''); + } + if (active === 2) { + form.setFieldValue('query', ''); + form.setFieldValue('mangaOrder', -1); + form.setFieldValue('mangaTitle', ''); + } + if (active === 3) { + form.setFieldValue('interval', ''); + } + setActive((current) => (current > 0 ? current - 1 : current)); + }; + + const onSubmit = form.onSubmit(async (values) => { + setVisible((v) => !v); + const { mangaOrder, mangaTitle, query, source, interval } = values; + try { + await mutation.mutateAsync({ + keyword: query, + order: mangaOrder, + title: mangaTitle, + interval, + source, + }); + } catch (err) { + showNotification({ + icon: , + color: 'red', + autoClose: true, + title: 'Manga', + message: ( + + Failed to create add manga. {`${err}`} + + ), + }); + + form.reset(); + onClose(); + setVisible((v) => !v); + return; + } + form.reset(); + onClose(); + setVisible((v) => !v); + showNotification({ + icon: , + color: 'teal', + autoClose: true, + title: 'Manga', + message: ( + + {values.mangaTitle} is added to library + + ), + }); + }); + + return ( +
+ + + + + + + + + ); +} diff --git a/src/components/addManga/index.tsx b/src/components/addManga/index.tsx new file mode 100644 index 0000000..ebb9618 --- /dev/null +++ b/src/components/addManga/index.tsx @@ -0,0 +1,55 @@ +import { createStyles, Paper, Tooltip } from '@mantine/core'; +import { useModals } from '@mantine/modals'; +import { IconPlus } from '@tabler/icons'; +import { AddMangaForm } from './form'; + +const useStyles = createStyles((theme) => ({ + card: { + height: 350, + width: 210, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.gray[4], + cursor: 'pointer', + + transition: 'transform 150ms ease, box-shadow 150ms ease', + + '&:hover': { + transform: 'scale(1.01)', + boxShadow: theme.shadows.md, + }, + }, +})); + +export function AddManga({ onAdd }: { onAdd: () => void }) { + const { classes } = useStyles(); + const modals = useModals(); + + const openCreateModal = () => { + const id = modals.openModal({ + overflow: 'inside', + trapFocus: true, + size: 'xl', + closeOnClickOutside: false, + title: 'Add a new manga', + centered: true, + children: ( + { + modals.closeModal(id); + onAdd(); + }} + /> + ), + }); + }; + + return ( + + + + + + ); +} diff --git a/src/components/mangaSearchResult.tsx b/src/components/addManga/mangaSearchResult.tsx similarity index 100% rename from src/components/mangaSearchResult.tsx rename to src/components/addManga/mangaSearchResult.tsx diff --git a/src/components/addManga/steps/downloadStep.tsx b/src/components/addManga/steps/downloadStep.tsx new file mode 100644 index 0000000..a25644a --- /dev/null +++ b/src/components/addManga/steps/downloadStep.tsx @@ -0,0 +1,48 @@ +import { Box, LoadingOverlay, Select, Stack, TextInput } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form'; +import { IconFolderPlus } from '@tabler/icons'; +import { trpc } from '../../../utils/trpc'; +import type { FormType } from '../form'; + +const availableIntervals = ['daily', 'hourly', 'weekly', 'minutely']; + +export function DownloadStep({ form }: { form: UseFormReturnType }) { + const libraryQuery = trpc.library.query.useQuery(); + + const libraryPath = libraryQuery.data?.path; + + const intervalSelectData = availableIntervals.map((k) => ({ label: k, value: k })); + + if (libraryQuery.isLoading) { + return ; + } + + const sanitizeMangaName = form.values.mangaTitle + .replaceAll(/[\\/<>:;"'|?!*{}#%&^+,~\s]/g, '_') + .replaceAll(/__+/g, '_') + .replaceAll(/^[_\-.]+|[_\-.]+$/g, '_'); + + const downloadPath = `${libraryPath}/${sanitizeMangaName}`; + + return ( + + + + + ); +} diff --git a/src/components/newMangaCard.tsx b/src/components/newMangaCard.tsx deleted file mode 100644 index e341832..0000000 --- a/src/components/newMangaCard.tsx +++ /dev/null @@ -1,533 +0,0 @@ -import { - ActionIcon, - Badge, - Box, - Button, - Code, - createStyles, - Divider, - Grid, - Group, - Image, - LoadingOverlay, - Paper, - Select, - Stack, - 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, IconCheck, IconFolderPlus, IconPlus, 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: { - height: 350, - width: 210, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: theme.colors.gray[4], - cursor: 'pointer', - - transition: 'transform 150ms ease, box-shadow 150ms ease', - - '&:hover': { - transform: 'scale(1.01)', - 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', - }, -})); - -const availableIntervals = ['daily', 'hourly', 'weekly', 'minutely']; - -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' }), - interval: z.string().min(1, { message: 'Please select an interval' }), -}); - -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 ( - - - } - value={downloadPath} - /> - - - ); -} - -function SearchStep({ form }: { form: UseFormReturnType }) { - const ctx = trpc.useContext(); - type SearchResult = Awaited>; - - const [loading, setLoading] = useState(false); - const [searchResult, setSearchResult] = useState([]); - - const handleSearch = async () => { - form.validateField('query'); - if (!form.isValid('query')) { - return; - } - setLoading(true); - const result = await ctx.manga.search.fetch({ - keyword: form.values.query, - source: form.values.source, - }); - setLoading(false); - - if (result) { - setSearchResult(result); - } - }; - - return ( - <> - - - } - rightSection={ - - - - } - rightSectionWidth={42} - label="Search for a manga" - placeholder="Bleach" - {...form.getInputProps('query')} - /> -