-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
576 additions
and
535 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof schema>; | ||
|
||
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: <IconX size={18} />, | ||
color: 'red', | ||
autoClose: true, | ||
title: 'Manga', | ||
message: ( | ||
<Text> | ||
Failed to create add manga. <Code color="red">{`${err}`}</Code> | ||
</Text> | ||
), | ||
}); | ||
|
||
form.reset(); | ||
onClose(); | ||
setVisible((v) => !v); | ||
return; | ||
} | ||
form.reset(); | ||
onClose(); | ||
setVisible((v) => !v); | ||
showNotification({ | ||
icon: <IconCheck size={18} />, | ||
color: 'teal', | ||
autoClose: true, | ||
title: 'Manga', | ||
message: ( | ||
<Text> | ||
<Code color="blue">{values.mangaTitle}</Code> is added to library | ||
</Text> | ||
), | ||
}); | ||
}); | ||
|
||
return ( | ||
<form className={classes.form} onSubmit={onSubmit}> | ||
<AddMangaSteps form={form} active={active} setActive={setActive} /> | ||
<LoadingOverlay visible={visible} /> | ||
<Group position="right" className={classes.buttonGroup}> | ||
<Button variant="default" onClick={prevStep}> | ||
{active === 0 ? 'Cancel' : 'Back'} | ||
</Button> | ||
<Button hidden={active !== 3} type="submit"> | ||
Add | ||
</Button> | ||
<Button hidden={active === 3} onClick={nextStep}> | ||
Next step | ||
</Button> | ||
</Group> | ||
</form> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ( | ||
<AddMangaForm | ||
onClose={() => { | ||
modals.closeModal(id); | ||
onAdd(); | ||
}} | ||
/> | ||
), | ||
}); | ||
}; | ||
|
||
return ( | ||
<Tooltip label="Add a new manga" position="bottom"> | ||
<Paper shadow="lg" p="md" radius="md" className={classes.card} onClick={openCreateModal}> | ||
<IconPlus color="darkblue" opacity={0.5} size={96} /> | ||
</Paper> | ||
</Tooltip> | ||
); | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<FormType> }) { | ||
const libraryQuery = trpc.library.query.useQuery(); | ||
|
||
const libraryPath = libraryQuery.data?.path; | ||
|
||
const intervalSelectData = availableIntervals.map((k) => ({ label: k, value: k })); | ||
|
||
if (libraryQuery.isLoading) { | ||
return <LoadingOverlay visible />; | ||
} | ||
|
||
const sanitizeMangaName = form.values.mangaTitle | ||
.replaceAll(/[\\/<>:;"'|?!*{}#%&^+,~\s]/g, '_') | ||
.replaceAll(/__+/g, '_') | ||
.replaceAll(/^[_\-.]+|[_\-.]+$/g, '_'); | ||
|
||
const downloadPath = `${libraryPath}/${sanitizeMangaName}`; | ||
|
||
return ( | ||
<Box> | ||
<Stack> | ||
<Select | ||
data-autofocus | ||
size="sm" | ||
data={intervalSelectData} | ||
label="Download Interval" | ||
placeholder="Select an interval" | ||
{...form.getInputProps('interval')} | ||
/> | ||
<TextInput | ||
label="Location" | ||
size="sm" | ||
disabled | ||
icon={<IconFolderPlus size={18} stroke={1.5} />} | ||
value={downloadPath} | ||
/> | ||
</Stack> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { createStyles, Stepper } from '@mantine/core'; | ||
import { UseFormReturnType } from '@mantine/form'; | ||
import { Dispatch, SetStateAction } from 'react'; | ||
import type { FormType } from '../form'; | ||
import { DownloadStep } from './downloadStep'; | ||
import { ReviewStep } from './reviewStep'; | ||
import { SearchStep } from './searchStep'; | ||
import { SourceStep } from './sourceStep'; | ||
|
||
const useStyles = createStyles((_theme) => ({ | ||
stepper: { | ||
flexGrow: 1, | ||
}, | ||
stepBody: { | ||
marginTop: 30, | ||
marginBottom: 30, | ||
}, | ||
buttonGroup: { | ||
position: 'fixed', | ||
bottom: '19px', | ||
right: '55px', | ||
width: 'calc(100% - 55px)', | ||
height: '50px', | ||
background: 'white', | ||
}, | ||
})); | ||
|
||
export default function AddMangaSteps({ | ||
form, | ||
active, | ||
setActive, | ||
}: { | ||
form: UseFormReturnType<FormType>; | ||
active: number; | ||
setActive: Dispatch<SetStateAction<number>>; | ||
}) { | ||
const { classes } = useStyles(); | ||
|
||
return ( | ||
<Stepper | ||
classNames={{ | ||
root: classes.stepper, | ||
content: classes.stepBody, | ||
}} | ||
active={active} | ||
onStepClick={setActive} | ||
breakpoint="sm" | ||
m="xl" | ||
> | ||
<Stepper.Step | ||
label="Source" | ||
description={form.values.source || 'Select a source'} | ||
allowStepSelect={active > 0} | ||
color={active > 0 ? 'teal' : 'blue'} | ||
> | ||
<SourceStep form={form} /> | ||
</Stepper.Step> | ||
<Stepper.Step | ||
label="Manga" | ||
description={form.values.mangaTitle || 'Search for manga'} | ||
allowStepSelect={active > 1} | ||
color={active > 1 ? 'teal' : 'blue'} | ||
> | ||
<SearchStep form={form} /> | ||
</Stepper.Step> | ||
<Stepper.Step | ||
label="Download" | ||
description={form.values.interval || 'Select an interval'} | ||
allowStepSelect={active > 2} | ||
color={active > 2 ? 'teal' : 'blue'} | ||
> | ||
<DownloadStep form={form} /> | ||
</Stepper.Step> | ||
|
||
<Stepper.Completed> | ||
<ReviewStep form={form} /> | ||
</Stepper.Completed> | ||
</Stepper> | ||
); | ||
} |
Oops, something went wrong.