responsive navbar

This commit is contained in:
2025-05-01 11:10:31 +02:00
parent 10d1c91dea
commit 8f3a7009e1
19 changed files with 1202 additions and 47 deletions

View File

@@ -0,0 +1,13 @@
'use client'
import { Moon, Sun } from "lucide-react"
import { useEffect } from "react"
type Props = {activeTheme:string|undefined}
const ThemeIcon = (props:Props) => {
if (props.activeTheme == "dark") {
return (<Sun/>)
} else {
return (<Moon/>)
}
}
export default ThemeIcon;

View File

@@ -0,0 +1,22 @@
'use client'
import * as React from "react"
import { useTheme } from "next-themes"
import { Button } from "~/components/ui/button"
import ThemeIcon from "./ThemeIcon"
export function ThemeSwitch() {
const { setTheme, theme } = useTheme()
const toggleTheme = () => {
setTheme(theme == "dark" ? "light" : "dark")
}
return (
<>
<Button className="flex h-9 lg:h-full w-20 lg:w-12" variant="outline" size="icon" onClick={toggleTheme}>
<ThemeIcon activeTheme={theme}/>
<span className="sr-only">Toggle theme</span>
</Button>
</>
)
}

View File

@@ -1,25 +1,57 @@
import Link from "next/link"
import AdminWrap from "./AdminWrap"
import { SignedIn, SignedOut, SignUpButton, UserButton } from "@clerk/nextjs"
import { SignedIn, SignedOut, SignInButton, SignOutButton, SignUpButton, UserButton } from "@clerk/nextjs"
import { Button } from "~/components/ui/button"
import { ThemeSwitch } from "./ThemeSwitch"
export default function TopNav() {
return (
<nav className="flex flex-wrap items-center w-full border-b px-5 py-5 gap-5 bg-black text-white border-white">
<Link className="h-fit" href={"/blog"}> Blog </Link>
<Link className="h-fit" href={"/cv"}> CV </Link>
<Link className="h-fit" href={"/projects"}> Projects </Link>
<Link className="h-fit" href={"/fun"}> Fun </Link>
<div className="ml-auto"/>
<AdminWrap><Link className="h-fit" href={"/admin"}> Admin </Link></AdminWrap>
<div className="h-fit flex">
<SignedIn>
<UserButton/>
</SignedIn>
<SignedOut>
<SignUpButton/>
</SignedOut>
</div>
</nav>
<div className="absolute right-0 lg:relative">
<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">
<Link href={"/blog"}> Blog </Link>
</Button>
<Button asChild className="flex h-full w-full lg:w-20" variant="outline">
<Link href={"/cv"}> CV </Link>
</Button>
<Button asChild className="flex h-full w-full lg:w-20" variant="outline">
<Link href={"/projects"}> Projects </Link>
</Button>
<Button asChild className="flex h-full w-full lg:w-20" variant="outline">
<Link href={"/fun"}> Fun </Link>
</Button>
</div>
<div className="flex flex-col-reverse flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row lg:ml-auto">
<AdminWrap>
<Button className="flex h-full w-full lg:w-20" variant="outline">
<Link className="" href={"/admin"}> Admin </Link>
</Button>
</AdminWrap>
<SignedIn>
<Button asChild className="flex h-full w-full lg:w-20" variant={"outline"}>
<SignOutButton />
</Button>
</SignedIn>
<SignedOut>
<Button asChild className="flex h-full cursor-pointer lg:w-20" variant={"outline"}>
<SignInButton mode="modal" />
</Button>
<Button asChild className="flex h-full cursor-pointer lg:w-20" variant={"outline"}>
<SignUpButton mode="modal" />
</Button>
</SignedOut>
<ThemeSwitch />
<SignedIn>
<Button asChild className="flex h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
<div>
<UserButton />
</div>
</Button>
</SignedIn>
</div>
</nav>
</div>
)
}

View File

@@ -0,0 +1,23 @@
'use client'
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export default function ThemeProvider({children}:{children: React.ReactNode}) {
const [mounted,setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
})
if (mounted) {
return (
<NextThemesProvider attribute="class" defaultTheme="dark">
{children}
</NextThemesProvider>
)
} else {
return (
<>
{children}
</>
)
}
}

View File

@@ -3,7 +3,7 @@ import { SignedIn } from "@clerk/nextjs";
export default function AdminPage() {
return (
<SignedIn>
<main className="flex min-h-screen flex-col items-center justify-center bg-black text-white">
<main className="flex min-h-screen flex-col items-center justify-center">
<div>
hello admin
</div>

View File

@@ -3,15 +3,18 @@ import { trpc } from "~/app/_trpc/Client"
import CvEntry, { type CvEntryProps } from "./CvEntry"
import type { servTrpc } from "~/app/_trpc/ServerClient"
type CvCategoryProps = {
initialData: Awaited<ReturnType<typeof servTrpc.cvCategory.get>>,
initialData: Awaited<ReturnType<typeof servTrpc.cv.category.list>>,
children?: React.ReactElement<CvEntryProps>
}
export default function CvCategory(props:CvCategoryProps) {
const cvCategories = trpc.cvCategory.get.useQuery(undefined,{
const cvCategories = trpc.cv.category.list.useQuery(undefined,{
initialData: props.initialData,
refetchOnMount: false,
refetchOnReconnect: false,
// refetchOnMount: false,
// refetchOnReconnect: false,
});
if (cvCategories.isPending) {
return (<div> Loading ... </div>);
}
return (
<div>
{cvCategories.data.map((cat) => {

View File

@@ -1,7 +1,7 @@
import { servTrpc } from "~/app/_trpc/ServerClient"
import CvCategory from "./_components/CvCategory";
export default async function CvPage() {
const cvCategories = await servTrpc.cvCategory.get();
const cvCategories = await servTrpc.cv.category.list();
return (
<CvCategory initialData={cvCategories}>
<></>

View File

@@ -6,6 +6,9 @@ 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})
config.autoAddCss = false;
export const metadata: Metadata = {
title: "Gregor Lohaus",
@@ -26,13 +29,15 @@ export default function RootLayout({
return (
<ClerkProvider>
<TrpcProvider>
<html lang="en" className={`${geist.variable}`}>
<body className="flex flex-col gap-2 bg-black text-white">
<TopNav/>
{children}
{modal}
</body>
</html>
<ThemeProvider>
<html lang="en" className={`${geist.variable}`} suppressHydrationWarning>
<body className="flex flex-col bg-background text-foreground">
<TopNav />
{children}
{modal}
</body>
</html>
</ThemeProvider>
</TrpcProvider>
</ClerkProvider>
);

View File

@@ -1,6 +1,6 @@
export default function HomePage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-black text-white">
<main>
<div>
hello world
</div>

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:border-2 hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,10 +1,8 @@
import { router } from "../trpc";
import { CvCategoryRouter } from "./cvCategory";
import { publicProcedure } from "~/server/trpc";
import { CvRouter } from "./cv";
export const trpcRouter = router({
hello: publicProcedure.query(async () => "world"),
cvCategory: CvCategoryRouter
cv: CvRouter
})
export type TrpcRouter = typeof trpcRouter

View File

@@ -0,0 +1,45 @@
import { db } from "~/server/db";
import { publicProcedure, router } from "~/server/trpc";
import { cvCategory } from "~/server/db/schema";
import { createInsertSchema, createUpdateSchema, createSelectSchema} from 'drizzle-zod'
import { z } from 'zod'
import { eq } from "drizzle-orm";
const selectShema = createSelectSchema(cvCategory)
const insertShema = createInsertSchema(cvCategory)
const updateSchema = createUpdateSchema(cvCategory)
export const CategoryRouter = router({
list: publicProcedure.query(async () => {
const categories = await db.query.cvCategory.findMany({
orderBy: (model, {desc} ) => desc(model.name)
});
return categories;
}),
get: publicProcedure.input(selectShema.pick({id: true})).query(async (opts) => {
const { input } = opts
const category = await db.query.cvCategory.findFirst({
where: eq(cvCategory.id,input.id)
})
return category;
}),
create: publicProcedure.input(insertShema).mutation(async (opts) => {
const { input } = opts;
const category = await db.insert(cvCategory).values(input).returning().execute()
return category
}),
update: publicProcedure
.input(z.object({
by: selectShema.pick({id:true}),
update: updateSchema
}))
.mutation(async (opts) => {
const {input} = opts;
const category = await db.update(cvCategory)
.set(input.update)
.returning()
.where(eq(cvCategory.id,input.by.id))
return category
})
})

View File

@@ -0,0 +1,6 @@
import { router } from "~/server/trpc"
import { CategoryRouter } from "./category"
export const CvRouter = router({
category: CategoryRouter
})

View File

@@ -1,12 +0,0 @@
import { db } from "~/server/db";
import { publicProcedure, router } from "~/server/trpc";
export const CvCategoryRouter = router({
get: publicProcedure.query(async () => {
const categories = await db.query.cvCategory.findMany({
orderBy: (model, {desc} ) => desc(model.name)
});
return categories;
})
})

View File

@@ -1,6 +1,125 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.1rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}