This commit is contained in:
2025-04-08 00:13:48 +02:00
parent 0ad70b3c7d
commit cdd9f01214
11 changed files with 345 additions and 34 deletions

15
src/app/admin/page.tsx Normal file
View File

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

View File

@@ -1,12 +1,15 @@
import "~/styles/globals.css";
import Link from "next/link";
import type { Metadata } from "next";
import { Geist } from "next/font/google";
import { ClerkProvider, SignedIn, SignedOut, SignUpButton, UserButton } from "@clerk/nextjs";
import { auth } from "@clerk/nextjs/server";
import { env } from "~/env";
export const metadata: Metadata = {
title: "Create T3 App",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
title: "Gregor Lohaus",
description: "My Personal Website",
icons: [{ rel: "icon", url: "/GLIcon.svg" }],
};
const geist = Geist({
@@ -14,13 +17,32 @@ const geist = Geist({
variable: "--font-geist-sans",
});
const TopNav = () => {
const AdminWrap = async ({children,}: Readonly<{ children: React.ReactNode }>) => {
const userid = (await auth()).userId
const isAdmin = (userid == env.ADMIN_USER_CLERK_ID)
if (isAdmin) {
return <>{children}</>
}
return (<></>)
}
const TopNav = async () => {
return (
<nav className="flex w-full border-b p-4 gap-5 bg-black text-white border-white">
<div> Blog </div>
<div> CV </div>
<div> Projects </div>
<div> Fun </div>
<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>
)
}
@@ -29,11 +51,13 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${geist.variable}`}>
<body className="flex flex-col gap-2 bg-black text-white">
<TopNav/>
{children}
</body>
</html>
<ClerkProvider>
<html lang="en" className={`${geist.variable}`}>
<body className="flex flex-col gap-2 bg-black text-white">
<TopNav/>
{children}
</body>
</html>
</ClerkProvider>
);
}

View File

@@ -1,18 +1,10 @@
import Link from "next/link";
import { db } from "~/server/db";
export const dynamic = "force-dynamic"
export default async function HomePage() {
const posts = await db.query.posts.findMany()
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-black text-white">
<div>
{posts.map((post) => {
return (
<div key={post.id}>
{post.name}
</div>
)
})}
hello world
</div>
</main>
);

View File

@@ -8,6 +8,7 @@ export const env = createEnv({
*/
server: {
DATABASE_URL: z.string().url(),
ADMIN_USER_CLERK_ID: z.string(),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
@@ -28,6 +29,7 @@ export const env = createEnv({
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
ADMIN_USER_CLERK_ID: process.env.ADMIN_USER_CLERK_ID,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},

21
src/middleware.ts Normal file
View File

@@ -0,0 +1,21 @@
import { clerkMiddleware, createRouteMatcher, currentUser } from "@clerk/nextjs/server";
import { env } from "~/env";
const isTenantAdminRoute = createRouteMatcher(['/admin(.*)'])
export default clerkMiddleware(async (auth,req) => {
if (isTenantAdminRoute(req)) {
let userid = (await auth()).userId
if (userid != env.ADMIN_USER_CLERK_ID) {
await auth.protect()
}
}
});
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
};

1
src/server/db/query.ts Normal file
View File

@@ -0,0 +1 @@
import "server-only"

View File

@@ -1,7 +1,7 @@
// Example model schema from the Drizzle docs
// https://orm.drizzle.team/docs/sql-schema-declaration
import { sql } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import { index, pgTableCreator } from "drizzle-orm/pg-core";
/**
@@ -12,16 +12,37 @@ import { index, pgTableCreator } from "drizzle-orm/pg-core";
*/
export const createTable = pgTableCreator((name) => `gregorlohaus.com_${name}`);
export const posts = createTable(
"post",
export const cvCategory = createTable(
"cv_category",
(d) => ({
id: d.integer().primaryKey().generatedByDefaultAsIdentity(),
name: d.varchar({ length: 256 }),
id: d.uuid().primaryKey(),
name: d.varchar({length: 50})
}),
(t) => [index("name_idx").on(t.name)],
)
export const cvCategoryRelations = relations(cvCategory,({many}) => ({
cvEntry: many(cvEntry)
}))
export const cvEntry = createTable(
"cv_entry",
(d) => ({
id: d.uuid().primaryKey().notNull(),
categoryId: d.uuid('category_id'),
fromTime: d.date().notNull(),
toTime: d.date().notNull(),
createdAt: d
.timestamp({ withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()),
})
)
export const cvEntryRelations = relations(cvEntry, ({one}) => ({
category: one(cvCategory, {
fields: [cvEntry.categoryId],
references: [cvCategory.id]
}),
(t) => [index("name_idx").on(t.name)],
);
}));