feat(app): add changelog list, byId and create page
This commit is contained in:
parent
ee3faad379
commit
b2ef80d58d
@ -13,13 +13,16 @@
|
||||
"@boring.tools/schema": "workspace:*",
|
||||
"@boring.tools/ui": "workspace:*",
|
||||
"@clerk/clerk-react": "^5.9.4",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"@tanstack/react-router": "^1.58.15",
|
||||
"axios": "^1.7.7",
|
||||
"lucide-react": "^0.446.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"react-hook-form": "^7.53.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
|
@ -37,7 +37,6 @@ export const Navigation = () => {
|
||||
to={route.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.name}
|
||||
|
@ -28,7 +28,7 @@ export const useChangelogById = ({ id }: { id: string }) => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['changelogById', id],
|
||||
queryFn: async (): Promise<ReadOnlyDict<Changelog>> =>
|
||||
queryFn: async (): Promise<Readonly<Changelog>> =>
|
||||
await queryFetch({
|
||||
path: `changelog/${id}`,
|
||||
method: 'get',
|
||||
@ -44,7 +44,7 @@ export const useChangelogCreate = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
payload: ChangelogCreate,
|
||||
): Promise<ReadonlySet<Changelog>> =>
|
||||
): Promise<Readonly<Changelog>> =>
|
||||
await queryFetch({
|
||||
path: 'changelog',
|
||||
data: payload,
|
||||
@ -62,9 +62,7 @@ export const useChangelogRemove = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
}: { id: string }): Promise<ReadOnlyDict<Changelog>> =>
|
||||
mutationFn: async ({ id }: { id: string }): Promise<Readonly<Changelog>> =>
|
||||
await queryFetch({
|
||||
path: `changelog/${id}`,
|
||||
method: 'delete',
|
||||
|
@ -19,6 +19,8 @@ import { Route as rootRoute } from './routes/__root'
|
||||
const IndexLazyImport = createFileRoute('/')()
|
||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@ -39,6 +41,18 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||
import('./routes/changelog/index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||
path: '/changelog/create',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/changelog/create.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
||||
path: '/changelog/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/changelog/$id.lazy').then((d) => d.Route))
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@ -50,6 +64,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/$id': {
|
||||
id: '/changelog/$id'
|
||||
path: '/changelog/$id'
|
||||
fullPath: '/changelog/$id'
|
||||
preLoaderRoute: typeof ChangelogIdLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/create': {
|
||||
id: '/changelog/create'
|
||||
path: '/changelog/create'
|
||||
fullPath: '/changelog/create'
|
||||
preLoaderRoute: typeof ChangelogCreateLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/': {
|
||||
id: '/changelog/'
|
||||
path: '/changelog'
|
||||
@ -71,12 +99,16 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
}
|
||||
@ -84,27 +116,44 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||
'/user/': typeof UserIndexLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/changelog' | '/user'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/changelog'
|
||||
| '/user'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/changelog' | '/user'
|
||||
id: '__root__' | '/' | '/changelog/' | '/user/'
|
||||
to: '/' | '/changelog/$id' | '/changelog/create' | '/changelog' | '/user'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/changelog/'
|
||||
| '/user/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexLazyRoute: typeof IndexLazyRoute
|
||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRoute
|
||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexLazyRoute: IndexLazyRoute,
|
||||
ChangelogIdLazyRoute: ChangelogIdLazyRoute,
|
||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||
}
|
||||
@ -122,6 +171,8 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/changelog/$id",
|
||||
"/changelog/create",
|
||||
"/changelog/",
|
||||
"/user/"
|
||||
]
|
||||
@ -129,6 +180,12 @@ export const routeTree = rootRoute
|
||||
"/": {
|
||||
"filePath": "index.lazy.tsx"
|
||||
},
|
||||
"/changelog/$id": {
|
||||
"filePath": "changelog/$id.lazy.tsx"
|
||||
},
|
||||
"/changelog/create": {
|
||||
"filePath": "changelog/create.lazy.tsx"
|
||||
},
|
||||
"/changelog/": {
|
||||
"filePath": "changelog/index.lazy.tsx"
|
||||
},
|
||||
|
33
apps/app/src/routes/changelog/$id.lazy.tsx
Normal file
33
apps/app/src/routes/changelog/$id.lazy.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Button } from '@boring.tools/ui'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { useChangelogById } from '../../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const { id } = Route.useParams()
|
||||
const { data, error, isPending, refetch } = useChangelogById({ id })
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-32 flex-col">
|
||||
<h1 className="text-3xl">Changelogs</h1>
|
||||
<p>Please try again later</p>
|
||||
|
||||
<Button onClick={() => refetch()}>Retry</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{!isPending && data && (
|
||||
<div>
|
||||
<h1 className="text-3xl">{data.title}</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/changelog/$id')({
|
||||
component: Component,
|
||||
})
|
117
apps/app/src/routes/changelog/create.lazy.tsx
Normal file
117
apps/app/src/routes/changelog/create.lazy.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { ChangelogCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import type { z } from 'zod'
|
||||
import { useChangelogCreate } from '../../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const navigate = useNavigate({ from: '/changelog/create' })
|
||||
const changelogCreate = useChangelogCreate()
|
||||
const form = useForm<z.infer<typeof ChangelogCreateInput>>({
|
||||
resolver: zodResolver(ChangelogCreateInput),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
isSemver: true,
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof ChangelogCreateInput>) => {
|
||||
changelogCreate.mutate(values, {
|
||||
onSuccess(data) {
|
||||
navigate({ to: '/changelog/$id', params: { id: data.id } })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">New changelog</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My changelog" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the changelog..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSemver"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Semver</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is following the{' '}
|
||||
<a
|
||||
href="https://semver.org/lang/de/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
semantic versioning?
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Create</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/changelog/create')({
|
||||
component: Component,
|
||||
})
|
@ -1,27 +1,67 @@
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { PlusCircleIcon } from 'lucide-react'
|
||||
import { useChangelogList } from '../../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const { data, error, isPending } = useChangelogList()
|
||||
const { data, error, isPending, refetch } = useChangelogList()
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-32 flex-col">
|
||||
<h1 className="text-3xl">Changelogs</h1>
|
||||
<p>Please try again later</p>
|
||||
|
||||
<Button onClick={() => refetch()}>Retry</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Changelogs</h1>
|
||||
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return <div key={changelog.id}>{changelog.title}</div>
|
||||
})}
|
||||
<div className="flex gap-10 w-full">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return (
|
||||
<Link
|
||||
to="/changelog/$id"
|
||||
params={{ id: changelog.id }}
|
||||
key={changelog.id}
|
||||
>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{changelog.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center flex-col">
|
||||
<span>Versions: {changelog.computed.versionCount}</span>
|
||||
|
||||
<span>Commits: {changelog.computed.commitCount}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<Link to="/changelog/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New Changelog</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user