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/schema": "workspace:*",
|
||||||
"@boring.tools/ui": "workspace:*",
|
"@boring.tools/ui": "workspace:*",
|
||||||
"@clerk/clerk-react": "^5.9.4",
|
"@clerk/clerk-react": "^5.9.4",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@tanstack/react-query": "^5.59.0",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-router": "^1.58.15",
|
"@tanstack/react-router": "^1.58.15",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
|
@ -37,7 +37,6 @@ export const Navigation = () => {
|
|||||||
to={route.to}
|
to={route.to}
|
||||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||||
activeProps={{ className: 'bg-muted text-primary' }}
|
activeProps={{ className: 'bg-muted text-primary' }}
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
>
|
||||||
<route.icon className="h-4 w-4" />
|
<route.icon className="h-4 w-4" />
|
||||||
{route.name}
|
{route.name}
|
||||||
|
@ -28,7 +28,7 @@ export const useChangelogById = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['changelogById', id],
|
queryKey: ['changelogById', id],
|
||||||
queryFn: async (): Promise<ReadOnlyDict<Changelog>> =>
|
queryFn: async (): Promise<Readonly<Changelog>> =>
|
||||||
await queryFetch({
|
await queryFetch({
|
||||||
path: `changelog/${id}`,
|
path: `changelog/${id}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -44,7 +44,7 @@ export const useChangelogCreate = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (
|
mutationFn: async (
|
||||||
payload: ChangelogCreate,
|
payload: ChangelogCreate,
|
||||||
): Promise<ReadonlySet<Changelog>> =>
|
): Promise<Readonly<Changelog>> =>
|
||||||
await queryFetch({
|
await queryFetch({
|
||||||
path: 'changelog',
|
path: 'changelog',
|
||||||
data: payload,
|
data: payload,
|
||||||
@ -62,9 +62,7 @@ export const useChangelogRemove = () => {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ id }: { id: string }): Promise<Readonly<Changelog>> =>
|
||||||
id,
|
|
||||||
}: { id: string }): Promise<ReadOnlyDict<Changelog>> =>
|
|
||||||
await queryFetch({
|
await queryFetch({
|
||||||
path: `changelog/${id}`,
|
path: `changelog/${id}`,
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
|
@ -19,6 +19,8 @@ import { Route as rootRoute } from './routes/__root'
|
|||||||
const IndexLazyImport = createFileRoute('/')()
|
const IndexLazyImport = createFileRoute('/')()
|
||||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||||
|
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||||
|
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
@ -39,6 +41,18 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
|||||||
import('./routes/changelog/index.lazy').then((d) => d.Route),
|
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
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@ -50,6 +64,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexLazyImport
|
preLoaderRoute: typeof IndexLazyImport
|
||||||
parentRoute: typeof rootRoute
|
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/': {
|
'/changelog/': {
|
||||||
id: '/changelog/'
|
id: '/changelog/'
|
||||||
path: '/changelog'
|
path: '/changelog'
|
||||||
@ -71,12 +99,16 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
|
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||||
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
|
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||||
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
}
|
}
|
||||||
@ -84,27 +116,44 @@ export interface FileRoutesByTo {
|
|||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
|
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||||
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||||
'/user/': typeof UserIndexLazyRoute
|
'/user/': typeof UserIndexLazyRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/changelog' | '/user'
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/changelog/$id'
|
||||||
|
| '/changelog/create'
|
||||||
|
| '/changelog'
|
||||||
|
| '/user'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/changelog' | '/user'
|
to: '/' | '/changelog/$id' | '/changelog/create' | '/changelog' | '/user'
|
||||||
id: '__root__' | '/' | '/changelog/' | '/user/'
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/changelog/$id'
|
||||||
|
| '/changelog/create'
|
||||||
|
| '/changelog/'
|
||||||
|
| '/user/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexLazyRoute: typeof IndexLazyRoute
|
IndexLazyRoute: typeof IndexLazyRoute
|
||||||
|
ChangelogIdLazyRoute: typeof ChangelogIdLazyRoute
|
||||||
|
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexLazyRoute: IndexLazyRoute,
|
IndexLazyRoute: IndexLazyRoute,
|
||||||
|
ChangelogIdLazyRoute: ChangelogIdLazyRoute,
|
||||||
|
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||||
}
|
}
|
||||||
@ -122,6 +171,8 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
|
"/changelog/$id",
|
||||||
|
"/changelog/create",
|
||||||
"/changelog/",
|
"/changelog/",
|
||||||
"/user/"
|
"/user/"
|
||||||
]
|
]
|
||||||
@ -129,6 +180,12 @@ export const routeTree = rootRoute
|
|||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.lazy.tsx"
|
"filePath": "index.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/changelog/$id": {
|
||||||
|
"filePath": "changelog/$id.lazy.tsx"
|
||||||
|
},
|
||||||
|
"/changelog/create": {
|
||||||
|
"filePath": "changelog/create.lazy.tsx"
|
||||||
|
},
|
||||||
"/changelog/": {
|
"/changelog/": {
|
||||||
"filePath": "changelog/index.lazy.tsx"
|
"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'
|
import { useChangelogList } from '../../hooks/useChangelog'
|
||||||
|
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
const { data, error, isPending } = useChangelogList()
|
const { data, error, isPending, refetch } = useChangelogList()
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center mt-32 flex-col">
|
<div className="flex items-center justify-center mt-32 flex-col">
|
||||||
<h1 className="text-3xl">Changelogs</h1>
|
<h1 className="text-3xl">Changelogs</h1>
|
||||||
<p>Please try again later</p>
|
<p>Please try again later</p>
|
||||||
|
|
||||||
|
<Button onClick={() => refetch()}>Retry</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col gap-5">
|
||||||
<h1 className="text-3xl">Changelogs</h1>
|
<h1 className="text-3xl">Changelogs</h1>
|
||||||
|
|
||||||
{!isPending &&
|
<div className="flex gap-10 w-full">
|
||||||
data &&
|
{!isPending &&
|
||||||
data.map((changelog) => {
|
data &&
|
||||||
return <div key={changelog.id}>{changelog.title}</div>
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user