cv page in pretty good state, markdown/html support added to description
This commit is contained in:
@@ -69,7 +69,6 @@
|
|||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"marked-react": "^3.0.0",
|
|
||||||
"next": "15.4.0-canary.17",
|
"next": "15.4.0-canary.17",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
@@ -77,8 +76,10 @@
|
|||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.57.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.2",
|
"react-resizable-panels": "^3.0.2",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
|
|||||||
794
pnpm-lock.yaml
generated
794
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@ export default function CvPage() {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<CollapsibleCvEntryForm entry={undefined} />
|
<CollapsibleCvEntryForm entry={undefined} categoryId={cat.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ export default async function Page({params}:{params: Promise<{id:string}>}) {
|
|||||||
console.log(params)
|
console.log(params)
|
||||||
const {id} = await params;
|
const {id} = await params;
|
||||||
return (
|
return (
|
||||||
<UpdateCvEntryForm params={{id:id, className: undefined}}/>
|
<UpdateCvEntryForm id={id} className={undefined} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { CategoryRouterOutputs } from "~/server/routers/cv/category";
|
|||||||
import { type Element } from "~/lib/utils";
|
import { type Element } from "~/lib/utils";
|
||||||
import UpdateCvEntryForm from "./UpdateForm";
|
import UpdateCvEntryForm from "./UpdateForm";
|
||||||
import CreateCvEntryForm from "./CreateForm";
|
import CreateCvEntryForm from "./CreateForm";
|
||||||
export default function CollapsibleCvEntryForm(params:{entry:Element<Element<CategoryRouterOutputs['list']>['cvEntry']>|EntryRouterOutputs['get']|Element<EntryRouterOutputs['list']>|undefined}) {
|
export default function CollapsibleCvEntryForm(params:{entry:Element<Element<CategoryRouterOutputs['list']>['cvEntry']>|EntryRouterOutputs['get']|Element<EntryRouterOutputs['list']>|undefined, categoryId: string|undefined}) {
|
||||||
return (
|
return (
|
||||||
<Collapsible>
|
<Collapsible>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
@@ -19,8 +19,8 @@ export default function CollapsibleCvEntryForm(params:{entry:Element<Element<Cat
|
|||||||
<CollapsibleContent className="autoAlpha">
|
<CollapsibleContent className="autoAlpha">
|
||||||
{
|
{
|
||||||
params.entry ?
|
params.entry ?
|
||||||
<UpdateCvEntryForm params={{ id: params.entry.id, className: "w-full" }} key={params.entry.id} /> :
|
<UpdateCvEntryForm id={params.entry.id} className="w-full" key={params.entry.id} /> :
|
||||||
<CreateCvEntryForm className="w-full"/>
|
<CreateCvEntryForm className="w-full" categoryId={params.categoryId ? params.categoryId : undefined}/>
|
||||||
|
|
||||||
}
|
}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|||||||
@@ -14,17 +14,38 @@ import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover
|
|||||||
import { CalendarIcon } from "lucide-react";
|
import { CalendarIcon } from "lucide-react";
|
||||||
import { Calendar } from "~/components/ui/calendar";
|
import { Calendar } from "~/components/ui/calendar";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
import { Checkbox } from "~/components/ui/checkbox"
|
||||||
|
|
||||||
export default function CreateCvEntryForm(params:{className:string|undefined}) {
|
export default function CreateCvEntryForm(params:{className:string|undefined, categoryId:string|undefined}) {
|
||||||
const categories = trpc.cv.category.list.useQuery()
|
const categories = trpc.cv.category.list.useQuery()
|
||||||
|
console.log(params.categoryId)
|
||||||
|
const [categorySelectPlaceHolder,setCategorySelectPlaceHolder] = useState("Select category")
|
||||||
|
const [categorySelectDefaultValue,setCategorySelectDefaultValue] = useState("")
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('category success effect')
|
||||||
|
if (categories.data !== undefined) {
|
||||||
|
setCategorySelectDefaultValue(categories.data[0]?.id? categories.data[0].id : "")
|
||||||
|
setCategorySelectPlaceHolder(categories.data[0]?.name? categories.data[0].name : "Select category")
|
||||||
|
}
|
||||||
|
if (params.categoryId) {
|
||||||
|
let matching = categories.data?.filter((c) => {
|
||||||
|
return c.id == params.categoryId
|
||||||
|
})
|
||||||
|
if (matching !== undefined && matching.length > 0) {
|
||||||
|
setCategorySelectDefaultValue(matching[0]?.id ? matching[0].id : categorySelectDefaultValue )
|
||||||
|
setCategorySelectPlaceHolder(matching[0]?.name ? matching[0].name : categorySelectPlaceHolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},[categories.isSuccess])
|
||||||
const form = useForm<z.infer<typeof insertSchemaForm>>({
|
const form = useForm<z.infer<typeof insertSchemaForm>>({
|
||||||
resolver: zodResolver(insertSchemaForm),
|
resolver: zodResolver(insertSchemaForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
categoryId: ""
|
categoryId: params.categoryId ? params.categoryId : "",
|
||||||
|
hideDates: false,
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,15 +88,15 @@ export default function CreateCvEntryForm(params:{className:string|undefined}) {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
{
|
{
|
||||||
categories.data ? (
|
categories.data ? (
|
||||||
<Select onValueChange={field.onChange} defaultValue={categories.data[0]?.id}>
|
<Select onValueChange={field.onChange} defaultValue={categorySelectDefaultValue}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={categories.data[0]?.name ? categories.data[0]?.name : "Select category" } />
|
<SelectValue placeholder={categorySelectPlaceHolder} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{ categories.data?.map((c) => {
|
{ categories.data?.map((c) => {
|
||||||
return (<SelectItem value={c.id}> {c.name} </SelectItem>)
|
return (<SelectItem key={c.id} value={c.id}> {c.name} </SelectItem>)
|
||||||
})}
|
})}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -194,6 +215,21 @@ export default function CreateCvEntryForm(params:{className:string|undefined}) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hideDates"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Hide dates</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value ? field.value : false}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button type="submit"> Create </Button>
|
<Button type="submit"> Create </Button>
|
||||||
<FormMessage className={mutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
<FormMessage className={mutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||||
{mutation.error ? mutation.error.message : mutation.status}
|
{mutation.error ? mutation.error.message : mutation.status}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import { CalendarIcon, Delete } from "lucide-react";
|
|||||||
import { Calendar } from "~/components/ui/calendar";
|
import { Calendar } from "~/components/ui/calendar";
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
export default function UpdateCvEntryForm({ params }: { params: { id: string, className: string | undefined } }) {
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
|
export default function UpdateCvEntryForm(params : { id: string, className: string | undefined }) {
|
||||||
console.log(params)
|
console.log(params)
|
||||||
const id = params.id
|
const id = params.id
|
||||||
const categories = trpc.cv.category.list.useQuery()
|
const categories = trpc.cv.category.list.useQuery()
|
||||||
@@ -59,7 +60,7 @@ export default function UpdateCvEntryForm({ params }: { params: { id: string, cl
|
|||||||
deleteMutation.mutate(id)
|
deleteMutation.mutate(id)
|
||||||
}
|
}
|
||||||
function onSubmit(values: z.infer<typeof updateRouteSchemaCliennt>) {
|
function onSubmit(values: z.infer<typeof updateRouteSchemaCliennt>) {
|
||||||
let { title, categoryId, description } = values.update;
|
let { title, categoryId, description, hideDates } = values.update;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
by: { id: id },
|
by: { id: id },
|
||||||
update: {
|
update: {
|
||||||
@@ -67,7 +68,8 @@ export default function UpdateCvEntryForm({ params }: { params: { id: string, cl
|
|||||||
toTime: format(values.update.toTime, 'yyyy-MM-dd'),
|
toTime: format(values.update.toTime, 'yyyy-MM-dd'),
|
||||||
title: title,
|
title: title,
|
||||||
categoryId: categoryId,
|
categoryId: categoryId,
|
||||||
description: description
|
description: description,
|
||||||
|
hideDates: hideDates
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -222,6 +224,21 @@ export default function UpdateCvEntryForm({ params }: { params: { id: string, cl
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="update.hideDates"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Hide dates</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value ? field.value : false}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button type="submit"> Update </Button>
|
<Button type="submit"> Update </Button>
|
||||||
<FormMessage className={cn(updateMutation.status == "success" ? "text-green-500" : "text-red-500", "flex flex-row justify-between")}>
|
<FormMessage className={cn(updateMutation.status == "success" ? "text-green-500" : "text-red-500", "flex flex-row justify-between")}>
|
||||||
{updateMutation.error ? updateMutation.error.message : updateMutation.status}
|
{updateMutation.error ? updateMutation.error.message : updateMutation.status}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import CvEntry, { type CvEntryProps } from "./CvEntry"
|
|||||||
import type { servTrpc } from "~/app/_trpc/ServerClient"
|
import type { servTrpc } from "~/app/_trpc/ServerClient"
|
||||||
import type { inferProcedureOutput } from "@trpc/server"
|
import type { inferProcedureOutput } from "@trpc/server"
|
||||||
import type { RouterOutputs } from "~/server/routers/_app"
|
import type { RouterOutputs } from "~/server/routers/_app"
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
|
||||||
type CvCategoryProps = {
|
type CvCategoryProps = {
|
||||||
initialData: RouterOutputs['cv']['category']['get'],
|
initialData: RouterOutputs['cv']['category']['get'],
|
||||||
@@ -14,14 +15,14 @@ export default function CvCategory(props:CvCategoryProps) {
|
|||||||
const query = trpc.cv.category.get.useQuery({id: props.initialData? props.initialData.id : ""});
|
const query = trpc.cv.category.get.useQuery({id: props.initialData? props.initialData.id : ""});
|
||||||
if (query.data !== undefined) {
|
if (query.data !== undefined) {
|
||||||
return (
|
return (
|
||||||
<Card className="gsapan">
|
<Card className={props.layout == "row" ? "w-full" : ""}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
{query.data?.name}
|
{query.data?.name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(query.data?.cvEntry.length ? query.data?.cvEntry.length : 0 ) > 0 ?
|
{(query.data?.cvEntry.length ? query.data?.cvEntry.length : 0 ) > 0 ?
|
||||||
<CardContent>
|
<CardContent className={cn(props.layout == "row" ? "flex flex-row" : "flex flex-col","gap-[1rem]")}>
|
||||||
{query.data?.cvEntry.map((entry) => (
|
{query.data?.cvEntry.map((entry) => (
|
||||||
<CvEntry key={entry.id} initialData={entry}/>
|
<CvEntry key={entry.id} initialData={entry}/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { trpc } from "~/app/_trpc/Client"
|
import { trpc } from "~/app/_trpc/Client"
|
||||||
import { Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
|
||||||
import { Skeleton } from "~/components/ui/skeleton"
|
import { Skeleton } from "~/components/ui/skeleton"
|
||||||
import type { Element } from "~/lib/utils"
|
import type { Element } from "~/lib/utils"
|
||||||
import type { cvEntry } from "~/server/db/schema"
|
|
||||||
import type { CategoryRouterOutputs } from "~/server/routers/cv/category"
|
import type { CategoryRouterOutputs } from "~/server/routers/cv/category"
|
||||||
import type { EntryRouterOutputs } from "~/server/routers/cv/entry"
|
import type { EntryRouterOutputs } from "~/server/routers/cv/entry"
|
||||||
import Markdown from 'marked-react'
|
import Markdown from 'react-markdown'
|
||||||
import { useEffect, useState } from "react"
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
export default function CvEntry(params: {
|
export default function CvEntry(params: {
|
||||||
initialData: EntryRouterOutputs['get'] | Element<Element<CategoryRouterOutputs['list']>['cvEntry']>
|
initialData: EntryRouterOutputs['get'] | Element<Element<CategoryRouterOutputs['list']>['cvEntry']>
|
||||||
}) {
|
}) {
|
||||||
@@ -18,17 +19,29 @@ export default function CvEntry(params: {
|
|||||||
data ?
|
data ?
|
||||||
<>
|
<>
|
||||||
<Card className="w-fit">
|
<Card className="w-fit">
|
||||||
<CardHeader>
|
{
|
||||||
<CardTitle> {data.title} </CardTitle>
|
data.title ?
|
||||||
</CardHeader>
|
<CardHeader>
|
||||||
<CardContent>
|
<CardTitle> {data.title} </CardTitle>
|
||||||
<div>
|
</CardHeader> :
|
||||||
<Markdown>{data.description ? data.description : undefined}</Markdown>
|
<></>
|
||||||
</div>
|
}
|
||||||
</CardContent>
|
{
|
||||||
<CardFooter className="text-sm">
|
data.description ?
|
||||||
{`${data.fromTime}-${data.toTime}`}
|
<CardContent>
|
||||||
</CardFooter>
|
<div>
|
||||||
|
<Markdown rehypePlugins={[rehypeRaw]}>{data.description}</Markdown>
|
||||||
|
</div>
|
||||||
|
</CardContent> :
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!data.hideDates ?
|
||||||
|
<CardFooter className="text-sm">
|
||||||
|
{`von ${format((new Date()).setTime(Date.parse(data.fromTime)), 'M. yyyy')} bis zum ${format((new Date()).setTime(Date.parse(data.toTime)), 'M. yyyy')}`}
|
||||||
|
</CardFooter> :
|
||||||
|
<></>
|
||||||
|
}
|
||||||
</Card>
|
</Card>
|
||||||
</> :
|
</> :
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { SidebarContent, SidebarProvider, Sidebar } from "~/components/ui/sideb
|
|||||||
import SidebarTriggerDisappearsOnMobile from "./_components/SidebarTriggerDisappearsOnMobile";
|
import SidebarTriggerDisappearsOnMobile from "./_components/SidebarTriggerDisappearsOnMobile";
|
||||||
import CvCategory from "./_components/CvCategory";
|
import CvCategory from "./_components/CvCategory";
|
||||||
export default function CvPage() {
|
export default function CvPage() {
|
||||||
|
//TODO fix display when one column is empty
|
||||||
|
// useState to store filtered categories
|
||||||
|
// should make this easier
|
||||||
const categories = trpc.cv.category.list.useQuery();
|
const categories = trpc.cv.category.list.useQuery();
|
||||||
const gsap = useGsapContext()
|
const gsap = useGsapContext()
|
||||||
const container = useRef<HTMLDivElement>(null)
|
const container = useRef<HTMLDivElement>(null)
|
||||||
@@ -60,9 +63,9 @@ export default function CvPage() {
|
|||||||
</> :
|
</> :
|
||||||
<></>
|
<></>
|
||||||
}
|
}
|
||||||
<div className="h-full w-full flex flex-wrap flex-row p-2 ">
|
<div className="h-full w-full flex flex-wrap flex-row p-[1rem] pt-[2rem] ">
|
||||||
<div id="mainwrap" className="flex w-full flex-col gap-[1rem]">
|
<div id="mainwrap" className="flex w-full flex-col gap-[1rem]">
|
||||||
<div id="header" className="flex w-full h-fit flex-row">
|
<div id="header" className="flex w-full h-fit flex-row gap-[1rem]">
|
||||||
{categories.data.filter((cat) => cat.layoutPosition == 'header').map((cat) => {
|
{categories.data.filter((cat) => cat.layoutPosition == 'header').map((cat) => {
|
||||||
return (
|
return (
|
||||||
<CvCategory layout="row" initialData={cat} key={cat.id} />
|
<CvCategory layout="row" initialData={cat} key={cat.id} />
|
||||||
@@ -70,7 +73,7 @@ export default function CvPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div id="colwrapper" className="flex flex-col md:flex-row w-full h-3/4 gap-[1rem]">
|
<div id="colwrapper" className="flex flex-col md:flex-row w-full h-3/4 gap-[1rem]">
|
||||||
<div id="col1" className="flex flex-col w-full h-full">
|
<div id="col1" className="flex flex-col w-full md:w-1/2 h-full gap-[1rem]">
|
||||||
{categories.data.filter((cat) => cat.layoutPosition == 'col1').map((cat) => {
|
{categories.data.filter((cat) => cat.layoutPosition == 'col1').map((cat) => {
|
||||||
return (
|
return (
|
||||||
<CvCategory layout="col" initialData={cat} key={cat.id} />
|
<CvCategory layout="col" initialData={cat} key={cat.id} />
|
||||||
@@ -78,7 +81,7 @@ export default function CvPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{categories.data.filter((cat) => cat.layoutPosition == 'col2').length > 0 ?
|
{categories.data.filter((cat) => cat.layoutPosition == 'col2').length > 0 ?
|
||||||
<div id="col2" className="flex flex-wrap flex-col w-full lg:w-1/2 h-full">
|
<div id="col2" className="flex flex-col w-full md:w-1/2 h-full gap-[1rem]">
|
||||||
{categories.data.filter((cat) => cat.layoutPosition == 'col2').map((cat) => {
|
{categories.data.filter((cat) => cat.layoutPosition == 'col2').map((cat) => {
|
||||||
return (
|
return (
|
||||||
<CvCategory layout="col" initialData={cat} key={cat.id} />
|
<CvCategory layout="col" initialData={cat} key={cat.id} />
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const cvEntry = createTable(
|
|||||||
toTime: d.date().notNull(),
|
toTime: d.date().notNull(),
|
||||||
title: d.varchar({length:50}).notNull(),
|
title: d.varchar({length:50}).notNull(),
|
||||||
description: d.text(),
|
description: d.text(),
|
||||||
|
hideDates: d.boolean(),
|
||||||
createdAt: d
|
createdAt: d
|
||||||
.timestamp({ withTimezone: true })
|
.timestamp({ withTimezone: true })
|
||||||
.default(sql`CURRENT_TIMESTAMP`)
|
.default(sql`CURRENT_TIMESTAMP`)
|
||||||
|
|||||||
Reference in New Issue
Block a user