diff --git a/apps/app/src/hooks/useChangelog.ts b/apps/app/src/hooks/useChangelog.ts index 1e0b681..5b5379f 100644 --- a/apps/app/src/hooks/useChangelog.ts +++ b/apps/app/src/hooks/useChangelog.ts @@ -131,3 +131,43 @@ export const useChangelogVersionCreate = () => { }, }) } + +export const useChangelogVersionById = ({ id }: { id: string }) => { + const { getToken } = useAuth() + + return useQuery({ + queryKey: ['changelogVersionById', id], + queryFn: async (): Promise<Readonly<Version>> => + await queryFetch({ + path: `changelog/version/${id}`, + method: 'get', + token: await getToken(), + }), + }) +} + +export const useChangelogVersionUpdate = () => { + const { getToken } = useAuth() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + id, + payload, + }: { + id: string + payload: VersionUpdate + }): Promise<Readonly<Version>> => + await queryFetch({ + path: `changelog/version/${id}`, + data: payload, + method: 'put', + token: await getToken(), + }), + onSuccess: (data) => { + queryClient.invalidateQueries({ + queryKey: ['changelogById', data.id], + }) + }, + }) +} diff --git a/apps/app/src/routes/changelog.$id.index.lazy.tsx b/apps/app/src/routes/changelog.$id.index.lazy.tsx index 2f83ded..065749c 100644 --- a/apps/app/src/routes/changelog.$id.index.lazy.tsx +++ b/apps/app/src/routes/changelog.$id.index.lazy.tsx @@ -7,21 +7,9 @@ import { } from '@boring.tools/ui' import { Link, createLazyFileRoute } from '@tanstack/react-router' import { PlusCircleIcon } from 'lucide-react' +import { VersionStatus } from '../components/Changelog/VersionStatus' import { useChangelogById } from '../hooks/useChangelog' -const VersionStatus = ({ status }: { status: string }) => { - switch (status) { - case 'draft': - return <div className="w-3 h-3 rounded-full bg-amber-600" /> - case 'published': - return <div className="w-3 h-3 rounded-full bg-emerald-600" /> - case 'review': - return <div className="w-3 h-3 rounded-full bg-sky-600" /> - default: - return <div className="w-3 h-3 rounded-full bg-neutral-600" /> - } -} - const Component = () => { const { id } = Route.useParams() const { data, isPending } = useChangelogById({ id }) diff --git a/apps/app/src/routes/changelog.$id.version.$versionId.tsx b/apps/app/src/routes/changelog.$id.version.$versionId.tsx index c80eb84..f700570 100644 --- a/apps/app/src/routes/changelog.$id.version.$versionId.tsx +++ b/apps/app/src/routes/changelog.$id.version.$versionId.tsx @@ -1,10 +1,80 @@ -import { Button } from '@boring.tools/ui' -import { createFileRoute } from '@tanstack/react-router' -import { useChangelogById } from '../hooks/useChangelog' +import { VersionUpdateInput } from '@boring.tools/schema' +import { + Button, + Calendar, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + cn, +} from '@boring.tools/ui' +import { zodResolver } from '@hookform/resolvers/zod' +import { + BlockTypeSelect, + BoldItalicUnderlineToggles, + ListsToggle, + MDXEditor, + type MDXEditorMethods, + UndoRedo, + headingsPlugin, + listsPlugin, + quotePlugin, + thematicBreakPlugin, + toolbarPlugin, +} from '@mdxeditor/editor' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { useForm } from 'react-hook-form' +import type { z } from 'zod' +import { + useChangelogVersionById, + useChangelogVersionUpdate, +} from '../hooks/useChangelog' +import '@mdxeditor/editor/style.css' +import { format } from 'date-fns' +import { CalendarIcon } from 'lucide-react' +import { useEffect, useRef } from 'react' +import { VersionStatus } from '../components/Changelog/VersionStatus' const Component = () => { - const { id } = Route.useParams() - const { data, error, isPending, refetch } = useChangelogById({ id }) + const { id, versionId } = Route.useParams() + const mdxEditorRef = useRef<MDXEditorMethods>(null) + const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` }) + const versionUpdate = useChangelogVersionUpdate() + const { data, error, isPending, refetch } = useChangelogVersionById({ + id: versionId, + }) + const form = useForm<z.infer<typeof VersionUpdateInput>>({ + resolver: zodResolver(VersionUpdateInput), + defaultValues: data, + }) + + const onSubmit = (values: z.infer<typeof VersionUpdateInput>) => { + versionUpdate.mutate( + { id: versionId, payload: values }, + { + onSuccess() { + navigate({ to: '/changelog/$id', params: { id } }) + }, + }, + ) + } + + useEffect(() => { + if (data) { + mdxEditorRef.current?.setMarkdown(data.markdown) + form.reset(data) + } + }, [data, form.reset]) if (error) { return ( @@ -19,7 +89,153 @@ const Component = () => { return ( <div className="flex flex-col gap-5"> - {!isPending && data && <div>version page</div>} + <hr /> + {!isPending && data && ( + <div> + <h1 className="text-xl mb-2">Version: {data.version}</h1> + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="space-y-8 max-w-screen-md" + > + <FormField + control={form.control} + name="markdown" + render={({ field }) => ( + <FormItem> + <FormLabel>Notes</FormLabel> + <FormControl> + <MDXEditor + className="dark-theme" + contentEditableClassName="prose dark:prose-invert max-w-none" + markdown={''} + ref={mdxEditorRef} + onChange={field.onChange} + onBlur={field.onBlur} + plugins={[ + headingsPlugin(), + listsPlugin(), + thematicBreakPlugin(), + quotePlugin(), + + toolbarPlugin({ + toolbarContents: () => ( + <> + <BlockTypeSelect /> + <BoldItalicUnderlineToggles /> + <ListsToggle /> + <UndoRedo /> + </> + ), + }), + ]} + /> + </FormControl>{' '} + <FormMessage /> + </FormItem> + )} + /> + + <div className="flex gap-5 items-center"> + <FormField + control={form.control} + name="status" + render={({ field }) => ( + <FormItem> + <FormLabel>Status</FormLabel> + <Select + onValueChange={field.onChange} + defaultValue={field.value} + value={field.value} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Select your version status" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="draft"> + <div className="flex gap-2 items-center"> + <VersionStatus status={'draft'} /> + <span>Draft</span> + </div> + </SelectItem> + <SelectItem value="review"> + <div className="flex gap-2 items-center"> + <VersionStatus status={'review'} /> + <span>Review</span> + </div> + </SelectItem> + <SelectItem value="published"> + <div className="flex gap-2 items-center"> + <VersionStatus status={'published'} /> + <span>Published</span> + </div> + </SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="releasedAt" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel className="mb-2">Released at</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant={'outline'} + size={'lg'} + className={cn( + 'w-[240px] pl-3 text-left font-normal', + !field.value && 'text-muted-foreground', + )} + > + {field.value ? ( + format(field.value, 'PPP') + ) : ( + <span>Pick a date</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value as Date} + onSelect={(date) => field.onChange(date)} + weekStartsOn={1} + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <div className="flex gap-5"> + <Button + type="button" + variant={'ghost'} + onClick={() => + navigate({ to: '/changelog/$id', params: { id } }) + } + > + Cancel + </Button> + <Button type="submit">Update</Button> + </div> + </form> + </Form> + </div> + )} </div> ) } diff --git a/apps/app/src/routes/changelog.$id.versionCreate.lazy.tsx b/apps/app/src/routes/changelog.$id.versionCreate.lazy.tsx index a572c99..bceec4d 100644 --- a/apps/app/src/routes/changelog.$id.versionCreate.lazy.tsx +++ b/apps/app/src/routes/changelog.$id.versionCreate.lazy.tsx @@ -1,6 +1,7 @@ import { VersionCreateInput } from '@boring.tools/schema' import { Button, + Calendar, Form, FormControl, FormField, @@ -8,6 +9,15 @@ import { FormLabel, FormMessage, Input, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + cn, } from '@boring.tools/ui' import { zodResolver } from '@hookform/resolvers/zod' import { @@ -28,6 +38,9 @@ import { useForm } from 'react-hook-form' import type { z } from 'zod' import { useChangelogVersionCreate } from '../hooks/useChangelog' import '@mdxeditor/editor/style.css' +import { format } from 'date-fns' +import { CalendarIcon } from 'lucide-react' +import { VersionStatus } from '../components/Changelog/VersionStatus' const Component = () => { const { id } = Route.useParams() @@ -39,6 +52,7 @@ const Component = () => { changelogId: id, version: '', markdown: '', + status: 'draft', }, }) @@ -52,7 +66,8 @@ const Component = () => { return ( <div className="flex flex-col gap-5"> - <h1 className="text-3xl">New version</h1> + <hr /> + <h1 className="text-xl mb-2">New version</h1> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} @@ -108,7 +123,100 @@ const Component = () => { )} /> - <Button type="submit">Create</Button> + <div className="flex gap-5 items-center"> + <FormField + control={form.control} + name="status" + render={({ field }) => ( + <FormItem> + <FormLabel>Status</FormLabel> + <Select + onValueChange={field.onChange} + defaultValue={field.value} + value={field.value} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Select your version status" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="draft"> + <div className="flex gap-2 items-center"> + <VersionStatus status={'draft'} /> + <span>Draft</span> + </div> + </SelectItem> + <SelectItem value="review"> + <div className="flex gap-2 items-center"> + <VersionStatus status={'review'} /> + <span>Review</span> + </div> + </SelectItem> + <SelectItem value="published"> + <div className="flex gap-2 items-center"> + <VersionStatus status={'published'} /> + <span>Published</span> + </div> + </SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="releasedAt" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel className="mb-2">Released at</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant={'outline'} + size={'lg'} + className={cn( + 'w-[240px] pl-3 text-left font-normal', + !field.value && 'text-muted-foreground', + )} + > + {field.value ? ( + format(field.value, 'PPP') + ) : ( + <span>Pick a date</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value as Date} + onSelect={(date) => field.onChange(date)} + weekStartsOn={1} + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <div className="flex gap-5"> + <Button + type="button" + variant={'ghost'} + onClick={() => navigate({ to: '/changelog/$id', params: { id } })} + > + Cancel + </Button> + <Button type="submit">Create</Button> + </div> </form> </Form> </div>