backup
This commit is contained in:
@@ -6,7 +6,7 @@ import { ThemeSwitch } from "./ThemeSwitch"
|
||||
|
||||
export default function TopNav() {
|
||||
return (
|
||||
<div className="absolute right-0 lg:relative">
|
||||
<div className="fixed lg:w-full right-0 z-50 lg:bg-background">
|
||||
<nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full">
|
||||
<div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row">
|
||||
<Button className="flex h-fit lg:h-full w-full lg:w-20" asChild variant="outline">
|
||||
|
||||
19
src/app/_providers/GsapProvicer.tsx
Normal file
19
src/app/_providers/GsapProvicer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
import { useGSAP } from '@gsap/react'
|
||||
import gsap from 'gsap'
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
|
||||
gsap.registerPlugin(useGSAP)
|
||||
const GsapContext = createContext<typeof globalThis.gsap | null>(null)
|
||||
|
||||
export function useGsapContext() {
|
||||
return useContext(GsapContext)
|
||||
}
|
||||
|
||||
export default function GsapProvider({children}:{children:ReactNode}) {
|
||||
return (
|
||||
<GsapContext.Provider value={gsap}>
|
||||
{children}
|
||||
</GsapContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default function ThemeProvider({children}:{children: React.ReactNode}) {
|
||||
})
|
||||
if (mounted) {
|
||||
return (
|
||||
<NextThemesProvider attribute="class" defaultTheme="dark">
|
||||
<NextThemesProvider disableTransitionOnChange nonce="test" attribute="class" defaultTheme="dark">
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
|
||||
@@ -19,7 +19,11 @@ function getBaseUrl() {
|
||||
}
|
||||
|
||||
export default function TrpcProvider({children}:{children: React.ReactNode}) {
|
||||
const [queryClient] = useState(() => new QueryClient({}));
|
||||
const [queryClient] = useState(() => new QueryClient({ defaultOptions: {
|
||||
queries: {
|
||||
experimental_prefetchInRender: true
|
||||
}
|
||||
}}));
|
||||
const [trpcClient] = useState(() => {
|
||||
return trpc.createClient({
|
||||
links: [
|
||||
|
||||
73
src/app/admin/_components/AdminSideBar.tsx
Normal file
73
src/app/admin/_components/AdminSideBar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import Link from "next/link";
|
||||
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar";
|
||||
|
||||
export default async function AdminSideBar() {
|
||||
return (
|
||||
<>
|
||||
<SidebarProvider>
|
||||
<Sidebar className="z-[51]">
|
||||
<SidebarTrigger className="absolute z-[52] left-65 top-100" />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
CV
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={"/admin/cv/category/create"}> Create Category </Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={"/admin/cv/entry/create"}> Create Entry </Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={"/admin/cv/category/list"}> Category List </Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={"/admin/cv/entries"}> Entry List </Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
Projects
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={"/"}> Some Project Action </Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
Blog
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={"/"}> Some Blog Action </Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
125
src/app/admin/cv/category/[id]/UpdateForm.tsx
Normal file
125
src/app/admin/cv/category/[id]/UpdateForm.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
import { insertSchema, updateRouteSchema, updateSchema } from "~/lib/schema/cv/category"
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from "zod";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { useRouter } from "next/navigation";
|
||||
import { use } from "react";
|
||||
|
||||
export default function UpdateCvCategoryForm({params}:{params:{id:string}}) {
|
||||
const id = params.id
|
||||
console.log(id)
|
||||
const category = trpc.cv.category.get.useQuery({id:id})
|
||||
const form = useForm<z.infer<typeof updateRouteSchema>>({
|
||||
resolver: zodResolver(updateRouteSchema),
|
||||
defaultValues: {
|
||||
by: {id: id},
|
||||
update: {
|
||||
layoutPosition: category.data?.layoutPosition,
|
||||
name: category.data?.layoutPosition
|
||||
}
|
||||
}
|
||||
})
|
||||
category.promise.then((data) => {
|
||||
form.setValue("update.layoutPosition",data?.layoutPosition)
|
||||
form.setValue("update.name",data?.name)
|
||||
})
|
||||
const mutation = trpc.cv.category.update.useMutation({onSuccess: () => {
|
||||
category.refetch()
|
||||
}})
|
||||
function onSubmit(values: z.infer<typeof updateRouteSchema>) {
|
||||
mutation.mutate({
|
||||
by: {id: id},
|
||||
update: {
|
||||
layoutPosition: values.update.layoutPosition,
|
||||
name: values.update.name
|
||||
}
|
||||
})
|
||||
}
|
||||
if (category.data !== undefined) {
|
||||
return (
|
||||
<Card.Card className="w-5/6 lg:w-1/2">
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Update Category
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="update.name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="by.id"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input hidden onChange={field.onChange} value={field.value}/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="update.layoutPosition"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Layout Position
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value == null ? undefined : field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={form.getValues().update.layoutPosition} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{updateRouteSchema.shape.update.shape.layoutPosition.unwrap().unwrap().options.map((o) => (
|
||||
<SelectItem key={o} value={o}> {o} </SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<Button type="submit"> Update </Button>
|
||||
<FormMessage className={mutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||
{mutation.error ? mutation.error.message : mutation.status}
|
||||
</FormMessage>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
11
src/app/admin/cv/category/[id]/page.tsx
Normal file
11
src/app/admin/cv/category/[id]/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use server'
|
||||
|
||||
import UpdateCvCategoryForm from "./UpdateForm";
|
||||
|
||||
export default async function Page({params}:{params: Promise<{id:string}>}) {
|
||||
console.log(params)
|
||||
const {id} = await params;
|
||||
return (
|
||||
<UpdateCvCategoryForm params={{id:id}}/>
|
||||
)
|
||||
}
|
||||
87
src/app/admin/cv/category/create/page.tsx
Normal file
87
src/app/admin/cv/category/create/page.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
import { insertSchema } from "~/lib/schema/cv/category"
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from "zod";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as Card from '~/components/ui/card'
|
||||
|
||||
export default function CreateCvCategoryForm() {
|
||||
const form = useForm<z.infer<typeof insertSchema>>({
|
||||
resolver: zodResolver(insertSchema),
|
||||
defaultValues: {
|
||||
id: crypto.randomUUID(),
|
||||
layoutPosition: "col1"
|
||||
}
|
||||
})
|
||||
const mutation = trpc.cv.category.create.useMutation()
|
||||
function onSubmit(values: z.infer<typeof insertSchema>) {
|
||||
mutation.mutate(values)
|
||||
form.setValue("id",crypto.randomUUID())
|
||||
}
|
||||
return (
|
||||
<Card.Card className="w-5/6 lg:w-1/2">
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Create Category
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="layoutPosition"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Layout Position
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value == null ? undefined : field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={form.getValues().layoutPosition} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{insertSchema.shape.layoutPosition.unwrap().unwrap().options.map((o) => (
|
||||
<SelectItem key={o} value={o}> {o} </SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<Button type="submit"> Create </Button>
|
||||
<FormMessage className={mutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||
{mutation.error ? mutation.error.message : mutation.status}
|
||||
</FormMessage>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
}
|
||||
65
src/app/admin/cv/category/list/page.tsx
Normal file
65
src/app/admin/cv/category/list/page.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
import Link from "next/link";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { useGSAP } from '@gsap/react'
|
||||
import { useRef } from "react";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Delete } from 'lucide-react'
|
||||
export default function CvPage() {
|
||||
const cvCategories = trpc.cv.category.list.useQuery();
|
||||
const gsap = useGsapContext()
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const mut = trpc.cv.category.delete.useMutation({onSuccess: () => {
|
||||
console.log('success')
|
||||
cvCategories.refetch()
|
||||
}})
|
||||
const deleteCategory = (id: string) => {
|
||||
console.log(`deleting ${id}`)
|
||||
mut.mutate(id)
|
||||
}
|
||||
useGSAP(() => {
|
||||
gsap?.from('.gsapan', { x: -100, opacity: 0, duration: 0.5, stagger: { each: 0.3 } })
|
||||
}, { scope: container, dependencies: [cvCategories.status], revertOnUpdate: true });
|
||||
return (
|
||||
<div ref={container} className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
||||
{cvCategories.data == undefined ?
|
||||
<div className="gsapan"></div>
|
||||
:
|
||||
<>
|
||||
{cvCategories.data.map((cat) => {
|
||||
return (
|
||||
<Card.Card className="gsapan" key={cat.id}>
|
||||
<Link href={`/admin/cv/category/${cat.id}`}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Category Name : {cat.name}
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
</Link>
|
||||
<Card.CardContent className="flex flex-row">
|
||||
Category id : {cat.id} <br />
|
||||
Category Position : {cat.layoutPosition} <br />
|
||||
{
|
||||
cat.cvEntry.length > 0 ? (
|
||||
<>
|
||||
{cat.cvEntry.map((entry) => {
|
||||
<Link href={`/admin/cv/entry/${entry.id}`}> {entry.title} </Link>
|
||||
})}
|
||||
</>
|
||||
) : (<></>)
|
||||
}
|
||||
<Button
|
||||
className="ml-auto cursor-pointer" variant="destructive" onClick={() => { deleteCategory(cat.id) }}>
|
||||
<Delete />
|
||||
</Button>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
125
src/app/admin/cv/entry/[id]/UpdateForm.tsx
Normal file
125
src/app/admin/cv/entry/[id]/UpdateForm.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
import { insertSchema, updateRouteSchema, updateSchema } from "~/lib/schema/cv/category"
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from "zod";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { useRouter } from "next/navigation";
|
||||
import { use } from "react";
|
||||
|
||||
export default function UpdateCvCategoryForm({params}:{params:{id:string}}) {
|
||||
const id = params.id
|
||||
console.log(id)
|
||||
const category = trpc.cv.category.get.useQuery({id:id})
|
||||
const form = useForm<z.infer<typeof updateRouteSchema>>({
|
||||
resolver: zodResolver(updateRouteSchema),
|
||||
defaultValues: {
|
||||
by: {id: id},
|
||||
update: {
|
||||
layoutPosition: category.data?.layoutPosition,
|
||||
name: category.data?.layoutPosition
|
||||
}
|
||||
}
|
||||
})
|
||||
category.promise.then((data) => {
|
||||
form.setValue("update.layoutPosition",data?.layoutPosition)
|
||||
form.setValue("update.name",data?.name)
|
||||
})
|
||||
const mutation = trpc.cv.category.update.useMutation({onSuccess: () => {
|
||||
category.refetch()
|
||||
}})
|
||||
function onSubmit(values: z.infer<typeof updateRouteSchema>) {
|
||||
mutation.mutate({
|
||||
by: {id: id},
|
||||
update: {
|
||||
layoutPosition: values.update.layoutPosition,
|
||||
name: values.update.name
|
||||
}
|
||||
})
|
||||
}
|
||||
if (category.data !== undefined) {
|
||||
return (
|
||||
<Card.Card className="w-5/6 lg:w-1/2">
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Update Category
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="update.name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="by.id"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input hidden onChange={field.onChange} value={field.value}/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="update.layoutPosition"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Layout Position
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value == null ? undefined : field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={form.getValues().update.layoutPosition} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{updateRouteSchema.shape.update.shape.layoutPosition.unwrap().unwrap().options.map((o) => (
|
||||
<SelectItem key={o} value={o}> {o} </SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<Button type="submit"> Update </Button>
|
||||
<FormMessage className={mutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||
{mutation.error ? mutation.error.message : mutation.status}
|
||||
</FormMessage>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
11
src/app/admin/cv/entry/[id]/page.tsx
Normal file
11
src/app/admin/cv/entry/[id]/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use server'
|
||||
|
||||
import UpdateCvCategoryForm from "./UpdateForm";
|
||||
|
||||
export default async function Page({params}:{params: Promise<{id:string}>}) {
|
||||
console.log(params)
|
||||
const {id} = await params;
|
||||
return (
|
||||
<UpdateCvCategoryForm params={{id:id}}/>
|
||||
)
|
||||
}
|
||||
98
src/app/admin/cv/entry/create/page.tsx
Normal file
98
src/app/admin/cv/entry/create/page.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
import { insertSchema } from "~/lib/schema/cv/entry"
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { format } from 'date-fns'
|
||||
import { z } from "zod";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { Calendar } from "~/components/ui/calendar";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useState } from "react"
|
||||
|
||||
export default function CreateCvEntryForm() {
|
||||
const categories = trpc.cv.category.list.useQuery()
|
||||
const [categoyIds, setCategoryIds] = useState<string[]>([])
|
||||
const form = useForm<z.infer<typeof insertSchema>>({
|
||||
resolver: zodResolver(insertSchema),
|
||||
defaultValues: {
|
||||
id: crypto.randomUUID(),
|
||||
title: "",
|
||||
description: "",
|
||||
categoryId: ""
|
||||
|
||||
}
|
||||
})
|
||||
categories.promise.then((data) => {
|
||||
form.setValue("categoryId", data[0]?.id)
|
||||
setCategoryIds(data.map((cat) => cat.id))
|
||||
})
|
||||
const mutation = trpc.cv.entry.create.useMutation({
|
||||
onSuccess: (data) => { form.setValue("id", data[0] ? data[0].id : "") }
|
||||
})
|
||||
function onSubmit(values: z.infer<typeof insertSchema>) {
|
||||
mutation.mutate(values)
|
||||
form.setValue("id", crypto.randomUUID())
|
||||
}
|
||||
return (
|
||||
<Card.Card className="w-5/6 lg:w-1/2">
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Create Category
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Title
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="title" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Title
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="description" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="categoryId"
|
||||
>
|
||||
</FormField>
|
||||
<Button type="submit"> Create </Button>
|
||||
<FormMessage className={mutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||
{mutation.error ? mutation.error.message : mutation.status}
|
||||
</FormMessage>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
}
|
||||
65
src/app/admin/cv/entry/list/page.tsx
Normal file
65
src/app/admin/cv/entry/list/page.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
import Link from "next/link";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { useGSAP } from '@gsap/react'
|
||||
import { useRef } from "react";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Delete } from 'lucide-react'
|
||||
export default function CvPage() {
|
||||
const cvCategories = trpc.cv.category.list.useQuery();
|
||||
const gsap = useGsapContext()
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const mut = trpc.cv.category.delete.useMutation({onSuccess: () => {
|
||||
console.log('success')
|
||||
cvCategories.refetch()
|
||||
}})
|
||||
const deleteCategory = (id: string) => {
|
||||
console.log(`deleting ${id}`)
|
||||
mut.mutate(id)
|
||||
}
|
||||
useGSAP(() => {
|
||||
gsap?.from('.gsapan', { x: -100, opacity: 0, duration: 0.5, stagger: { each: 0.3 } })
|
||||
}, { scope: container, dependencies: [cvCategories.status], revertOnUpdate: true });
|
||||
return (
|
||||
<div ref={container} className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
||||
{cvCategories.data == undefined ?
|
||||
<div className="gsapan"></div>
|
||||
:
|
||||
<>
|
||||
{cvCategories.data.map((cat) => {
|
||||
return (
|
||||
<Card.Card className="gsapan" key={cat.id}>
|
||||
<Link href={`/admin/cv/category/${cat.id}`}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Category Name : {cat.name}
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
</Link>
|
||||
<Card.CardContent className="flex flex-row">
|
||||
Category id : {cat.id} <br />
|
||||
Category Position : {cat.layoutPosition} <br />
|
||||
{
|
||||
cat.cvEntry.length > 0 ? (
|
||||
<>
|
||||
{cat.cvEntry.map((entry) => {
|
||||
<Link href={`/admin/cv/entry/${entry.id}`}> {entry.title} </Link>
|
||||
})}
|
||||
</>
|
||||
) : (<></>)
|
||||
}
|
||||
<Button
|
||||
className="ml-auto cursor-pointer" variant="destructive" onClick={() => { deleteCategory(cat.id) }}>
|
||||
<Delete />
|
||||
</Button>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
src/app/admin/layout.tsx
Normal file
14
src/app/admin/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use server'
|
||||
|
||||
import AdminSideBar from "./_components/AdminSideBar";
|
||||
|
||||
export default async function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
||||
return (
|
||||
<>
|
||||
<AdminSideBar/>
|
||||
<main className="absolute flex items-center content-center justify-center flex-wrap w-[100vw] left-0 top-15">
|
||||
{children}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
'use server'
|
||||
|
||||
import { SignedIn } from "@clerk/nextjs";
|
||||
|
||||
export default function AdminPage() {
|
||||
export default async function AdminPage() {
|
||||
return (
|
||||
<SignedIn>
|
||||
<main className="flex min-h-screen flex-col items-center justify-center">
|
||||
|
||||
@@ -2,28 +2,41 @@
|
||||
import { trpc } from "~/app/_trpc/Client"
|
||||
import CvEntry, { type CvEntryProps } from "./CvEntry"
|
||||
import type { servTrpc } from "~/app/_trpc/ServerClient"
|
||||
import type { inferProcedureOutput } from "@trpc/server"
|
||||
import type { RouterOutputs } from "~/server/routers/_app"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
|
||||
type CvCategoryProps = {
|
||||
initialData: Awaited<ReturnType<typeof servTrpc.cv.category.list>>,
|
||||
initialData: RouterOutputs['cv']['category']['get'],
|
||||
layout: "row"|"col",
|
||||
children?: React.ReactElement<CvEntryProps>
|
||||
}
|
||||
export default function CvCategory(props:CvCategoryProps) {
|
||||
const cvCategories = trpc.cv.category.list.useQuery(undefined,{
|
||||
initialData: props.initialData,
|
||||
// refetchOnMount: false,
|
||||
// refetchOnReconnect: false,
|
||||
});
|
||||
if (cvCategories.isPending) {
|
||||
return (<div> Loading ... </div>);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{cvCategories.data.map((cat) => {
|
||||
return (
|
||||
<div key={cat.id}>
|
||||
{cat.name}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default function CvCategory(props:CvCategoryProps) {
|
||||
const query = trpc.cv.category.get.useQuery({id: props.initialData? props.initialData.id : ""});
|
||||
if (query.data !== undefined) {
|
||||
return (
|
||||
<Card className="gsapan">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{query.data?.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{(query.data?.cvEntry.length ? query.data?.cvEntry.length : 0 ) > 0 ?
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Card className="gsapan">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Loading ...
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { trpc } from "~/app/_trpc/Client"
|
||||
import type { cvEntry } from "~/server/db/schema"
|
||||
|
||||
export type CvEntryProps = typeof cvEntry
|
||||
|
||||
export default function CvEntry(cvEntry: CvEntryProps) {
|
||||
const query = trpc.cv.entry.list.useQuery()
|
||||
return (<></>)
|
||||
}
|
||||
|
||||
23
src/app/cv/_components/SidebarTriggerDisappearsOnMobile.tsx
Normal file
23
src/app/cv/_components/SidebarTriggerDisappearsOnMobile.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { SidebarTrigger, useSidebar } from "~/components/ui/sidebar";
|
||||
|
||||
export default function SidebarTriggerDisappearsOnMobile() {
|
||||
const { isMobile, openMobile, state } = useSidebar()
|
||||
if (!isMobile) {
|
||||
if (state == "expanded") {
|
||||
return (
|
||||
<SidebarTrigger className="fixed z-[52] left-65 top-100" />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<SidebarTrigger className="fixed z-[52] left-3 top-100" />
|
||||
)
|
||||
}
|
||||
} else if (!openMobile) {
|
||||
return (
|
||||
<SidebarTrigger className="fixed z-[52] left-3 top-100" />
|
||||
)
|
||||
} else {
|
||||
<>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode}>) {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,103 @@
|
||||
import { servTrpc } from "~/app/_trpc/ServerClient"
|
||||
'use client'
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import { useGsapContext } from "../_providers/GsapProvicer";
|
||||
import { trpc } from "../_trpc/Client";
|
||||
import { useRef, useState } from "react";
|
||||
import type { Element } from "~/lib/utils";
|
||||
import { Card } from "~/components/ui/card";
|
||||
import { SidebarContent, SidebarProvider, Sidebar, SidebarTrigger } from "~/components/ui/sidebar";
|
||||
import SidebarTriggerDisappearsOnMobile from "./_components/SidebarTriggerDisappearsOnMobile";
|
||||
import CvCategory from "./_components/CvCategory";
|
||||
export default async function CvPage() {
|
||||
const cvCategories = await servTrpc.cv.category.list();
|
||||
return (
|
||||
<CvCategory initialData={cvCategories}>
|
||||
<></>
|
||||
</CvCategory>
|
||||
)
|
||||
export default function CvPage() {
|
||||
const categories = trpc.cv.category.list.useQuery();
|
||||
const gsap = useGsapContext()
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
enum Direction {
|
||||
Left = 1,
|
||||
Up,
|
||||
Right,
|
||||
Down
|
||||
}
|
||||
const nextGsapConf = (direction: Direction) => {
|
||||
switch (direction) {
|
||||
case Direction.Left:
|
||||
return { x: -100, opacity: 0, duration: 0.5 }
|
||||
case Direction.Up:
|
||||
return { y: -100, opacity: 0, duration: 0.5 }
|
||||
case Direction.Right:
|
||||
return { x: 100, opacity: 0, duration: 0.5 }
|
||||
case Direction.Down:
|
||||
return { y: 100, opacity: 0, duration: 0.5 }
|
||||
}
|
||||
}
|
||||
|
||||
type DataElement = Element<typeof categories.data>
|
||||
useGSAP(() => {
|
||||
const items = gsap?.utils.toArray<GSAPTweenTarget>('.gsapan');
|
||||
const tl = gsap?.timeline();
|
||||
let dir = Direction.Left;
|
||||
items?.forEach(item => {
|
||||
tl?.from(item, nextGsapConf(dir))
|
||||
if (dir == Direction.Down) {
|
||||
dir = Direction.Left
|
||||
} else {
|
||||
dir = dir + 1
|
||||
}
|
||||
})
|
||||
}, { scope: container, dependencies: [categories.isFetched], revertOnUpdate: true })
|
||||
if (categories.data !== undefined) {
|
||||
return (
|
||||
<>
|
||||
<SidebarProvider ref={container}>
|
||||
{categories.data.filter((cat) => cat.layoutPosition == 'sidebar').length > 0 ?
|
||||
<>
|
||||
<SidebarTriggerDisappearsOnMobile />
|
||||
<Sidebar className="z-[51] gsapan">
|
||||
<SidebarContent className="p-2">
|
||||
{categories.data.filter((cat) => cat.layoutPosition == 'sidebar').map((cat) => {
|
||||
return (
|
||||
<CvCategory layout="col" initialData={cat} />
|
||||
)
|
||||
})}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</> :
|
||||
<></>
|
||||
}
|
||||
<div className="h-full w-full flex flex-wrap flex-row p-2 ">
|
||||
<div id="mainwrap" className="flex flex-wrap w-full flex-col">
|
||||
<div id="header" className="flex flex-wrap w-full h-1/4 flex-row">
|
||||
{categories.data.filter((cat) => cat.layoutPosition == 'header').map((cat) => {
|
||||
return (
|
||||
<CvCategory layout="row" initialData={cat} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div id="colwrapper" className="flex flex-wrap flex-row w-full h-3/4">
|
||||
<div id="col1" className="flex flex-col w-full lg:w-1/2 h-full">
|
||||
{categories.data.filter((cat) => cat.layoutPosition == 'col1').map((cat) => {
|
||||
return (
|
||||
<CvCategory layout="col" initialData={cat} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div id="col2" className="flex flex-wrap flex-col w-full lg:w-1/2 h-full">
|
||||
{categories.data.filter((cat) => cat.layoutPosition == 'col2').map((cat) => {
|
||||
return (
|
||||
<CvCategory layout="col" initialData={cat} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { config } from "@fortawesome/fontawesome-svg-core"
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css"
|
||||
import TopNav from "./_components/TopNav";
|
||||
import TrpcProvider from "./_trpc/TrpcProvider";
|
||||
import dynamic from "next/dynamic";
|
||||
const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
||||
|
||||
// import dynamic from "next/dynamic";
|
||||
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
||||
import ThemeProvider from './_providers/ThemeProvider'
|
||||
import GsapProvider from "./_providers/GsapProvicer";
|
||||
config.autoAddCss = false;
|
||||
export const metadata: Metadata = {
|
||||
title: "Gregor Lohaus",
|
||||
@@ -22,22 +23,27 @@ const geist = Geist({
|
||||
});
|
||||
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
modal
|
||||
}: Readonly<{ children: React.ReactNode, modal: React.ReactNode }>) {
|
||||
|
||||
return (
|
||||
<ClerkProvider>
|
||||
<TrpcProvider>
|
||||
<ThemeProvider>
|
||||
<html lang="en" className={`${geist.variable}`} suppressHydrationWarning>
|
||||
<head/>
|
||||
<body className="flex flex-col bg-background text-foreground">
|
||||
<TopNav />
|
||||
{children}
|
||||
{modal}
|
||||
</body>
|
||||
</html>
|
||||
<GsapProvider>
|
||||
<html lang="en" className={`${geist.variable}`} suppressHydrationWarning>
|
||||
<head />
|
||||
<body className="flex flex-col bg-background text-foreground">
|
||||
<TopNav />
|
||||
<main className="absolute lg:top-10 h-[100vh] w-[100vw]">
|
||||
{children}
|
||||
</main>
|
||||
{modal}
|
||||
</body>
|
||||
</html>
|
||||
</GsapProvider>
|
||||
</ThemeProvider>
|
||||
</TrpcProvider>
|
||||
</ClerkProvider>
|
||||
|
||||
Reference in New Issue
Block a user