5 Commits

Author SHA1 Message Date
65b9184a22 refactor animated background, fix type 2026-04-24 14:23:52 +02:00
ea7ddb8e51 fix admin redirect loop 2026-04-24 12:41:27 +02:00
bcefe397ca speedinsights next 2026-04-24 12:26:44 +02:00
8ce95f2b5c add speed insides 2026-04-24 12:23:31 +02:00
da43b31aa3 chat interface, validate user id 2026-04-24 12:11:29 +02:00
10 changed files with 250 additions and 202 deletions

View File

@@ -52,6 +52,7 @@
"@trpc/server": "^11.12.0", "@trpc/server": "^11.12.0",
"@uiw/react-md-editor": "^4.0.11", "@uiw/react-md-editor": "^4.0.11",
"@uploadthing/react": "^7.3.3", "@uploadthing/react": "^7.3.3",
"@vercel/speed-insights": "^2.0.0",
"ai": "^6.0.116", "ai": "^6.0.116",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -975,6 +976,8 @@
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@vercel/speed-insights": ["@vercel/speed-insights@2.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.5", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.5", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.5", "vitest": "4.1.5" }, "optionalPeers": ["@vitest/browser"] }, "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.5", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.5", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.5", "vitest": "4.1.5" }, "optionalPeers": ["@vitest/browser"] }, "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A=="],

View File

@@ -66,6 +66,7 @@
"@trpc/server": "^11.12.0", "@trpc/server": "^11.12.0",
"@uiw/react-md-editor": "^4.0.11", "@uiw/react-md-editor": "^4.0.11",
"@uploadthing/react": "^7.3.3", "@uploadthing/react": "^7.3.3",
"@vercel/speed-insights": "^2.0.0",
"ai": "^6.0.116", "ai": "^6.0.116",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View File

@@ -1,16 +1,10 @@
"use client"; "use client";
import React, { useRef, useEffect, useCallback, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
/* ─────────────────────────────────────────────
* Config — grayscale palettes
* ───────────────────────────────────────────── */
const PALETTES = { const PALETTES = {
dark: { dark: {
base: "#0a0a0a",
particles: [ particles: [
"rgba(255,255,255,0.70)", "rgba(255,255,255,0.70)",
"rgba(255,255,255,0.45)", "rgba(255,255,255,0.45)",
@@ -18,9 +12,9 @@ const PALETTES = {
"rgba(200,200,200,0.35)", "rgba(200,200,200,0.35)",
"rgba(255,255,255,0.22)", "rgba(255,255,255,0.22)",
], ],
grainOpacity: 0.05,
}, },
light: { light: {
base: "#f5f5f5",
particles: [ particles: [
"rgba(0,0,0,0.55)", "rgba(0,0,0,0.55)",
"rgba(0,0,0,0.35)", "rgba(0,0,0,0.35)",
@@ -28,22 +22,10 @@ const PALETTES = {
"rgba(80,80,80,0.25)", "rgba(80,80,80,0.25)",
"rgba(0,0,0,0.18)", "rgba(0,0,0,0.18)",
], ],
grainOpacity: 0.03,
}, },
} as const; } as const;
/* ─────────────────────────────────────────────
* Helpers
* ───────────────────────────────────────────── */
const isMobileDevice = (): boolean => {
if (typeof window === "undefined") return false;
return window.matchMedia("(pointer: coarse)").matches || window.innerWidth < 768;
};
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
/* ─────────────────────────────────────────────
* Particle
* ───────────────────────────────────────────── */
interface Particle { interface Particle {
angle: number; angle: number;
radius: number; radius: number;
@@ -55,33 +37,43 @@ interface Particle {
wobblePhase: number; wobblePhase: number;
} }
const spawnParticle = (): Particle => ({
angle: rand(0, Math.PI * 2),
radius: rand(30, 240),
speed: rand(0.003, 0.002) * (Math.random() > 0.5 ? 1 : -1),
size: rand(1.2, 4),
colorIndex: Math.floor(rand(0, 5)),
wobbleAmp: rand(6, 30),
wobbleSpeed: rand(0.008, 0.035),
wobblePhase: rand(0, Math.PI * 2),
});
/* ─────────────────────────────────────────────
* Component
* ───────────────────────────────────────────── */
interface AnimatedBackgroundContainerProps { interface AnimatedBackgroundContainerProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
/** Number of orbiting particles. Default 60 */
particleCount?: number; particleCount?: number;
/** Max orbit radius in px — controls how far particles spread from the cursor. Default 240 */
orbitRadius?: number; orbitRadius?: number;
/** How quickly particles catch up to cursor (01). Default 0.06 */
followSpeed?: number; followSpeed?: number;
/** Speed multiplier for mobile random anchor drift. Default 1 */
mobileSpeed?: number; mobileSpeed?: number;
} }
const DEFAULT_PARTICLE_COLORS: readonly string[] = PALETTES.dark.particles;
const PARTICLE_COLOR_COUNT = DEFAULT_PARTICLE_COLORS.length;
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
const isMobileDevice = () => {
if (typeof window === "undefined") {
return false;
}
return window.matchMedia("(pointer: coarse)").matches || window.innerWidth < 768;
};
const createParticle = (orbitRadius: number): Particle => {
const minRadius = Math.max(10, orbitRadius * 0.12);
return {
angle: rand(0, Math.PI * 2),
radius: rand(minRadius, orbitRadius),
speed: rand(0.002, 0.003) * (Math.random() > 0.5 ? 1 : -1),
size: rand(1.2, 4),
colorIndex: Math.floor(rand(0, PARTICLE_COLOR_COUNT)),
wobbleAmp: rand(orbitRadius * 0.025, orbitRadius * 0.12),
wobbleSpeed: rand(0.008, 0.035),
wobblePhase: rand(0, Math.PI * 2),
};
};
export default function AnimatedBackgroundContainer({ export default function AnimatedBackgroundContainer({
children, children,
className = "", className = "",
@@ -92,172 +84,225 @@ export default function AnimatedBackgroundContainer({
}: AnimatedBackgroundContainerProps) { }: AnimatedBackgroundContainerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const mousePos = useRef({ x: 0, y: 0 }); const frameRef = useRef(0);
const smoothMouse = useRef({ x: 0, y: 0 }); const animationFrameRef = useRef<number | null>(null);
const mobileAnchor = useRef({ x: 0, y: 0 }); const isMobileRef = useRef(false);
const mobileTarget = useRef({ x: 0, y: 0 }); const particlesRef = useRef<Particle[]>([]);
const isMobile = useRef(false); const mousePosRef = useRef({ x: 0, y: 0 });
const particles = useRef<Particle[]>([]); const smoothMouseRef = useRef({ x: 0, y: 0 });
const frame = useRef(0); const mobileAnchorRef = useRef({ x: 0, y: 0 });
const mobileTargetRef = useRef({ x: 0, y: 0 });
const followSpeedRef = useRef(followSpeed);
const mobileSpeedRef = useRef(mobileSpeed);
const particleColorsRef = useRef<readonly string[]>(DEFAULT_PARTICLE_COLORS);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
let isDark = resolvedTheme === "dark"; const isDark = resolvedTheme === undefined || resolvedTheme === "dark";
if (resolvedTheme == undefined) {
isDark = true;
}
const palette = isDark ? PALETTES.dark : PALETTES.light; const palette = isDark ? PALETTES.dark : PALETTES.light;
/* Spawn particles */
useEffect(() => { useEffect(() => {
const minR = Math.max(10, orbitRadius * 0.12); particleColorsRef.current = palette.particles;
particles.current = Array.from({ length: particleCount }, () => ({ }, [palette]);
...spawnParticle(),
radius: rand(minR, orbitRadius), useEffect(() => {
wobbleAmp: rand(orbitRadius * 0.025, orbitRadius * 0.12), followSpeedRef.current = followSpeed;
})); }, [followSpeed]);
useEffect(() => {
mobileSpeedRef.current = mobileSpeed;
}, [mobileSpeed]);
useEffect(() => {
particlesRef.current = Array.from({ length: particleCount }, () =>
createParticle(orbitRadius),
);
}, [particleCount, orbitRadius]); }, [particleCount, orbitRadius]);
/* Detect mobile & seed positions */ const seedPositions = useCallback(() => {
const container = containerRef.current;
if (!container) {
return;
}
const centerX = container.clientWidth / 2;
const centerY = container.clientHeight / 2;
mousePosRef.current = { x: centerX, y: centerY };
smoothMouseRef.current = { x: centerX, y: centerY };
mobileAnchorRef.current = { x: centerX, y: centerY };
mobileTargetRef.current = {
x: rand(centerX * 0.4, centerX * 1.6),
y: rand(centerY * 0.4, centerY * 1.6),
};
}, []);
const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const width = container.clientWidth;
const height = container.clientHeight;
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext("2d");
if (!ctx) {
return;
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
if (!mounted) {
seedPositions();
}
}, [mounted, seedPositions]);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
isMobile.current = isMobileDevice(); isMobileRef.current = isMobileDevice();
if (containerRef.current) { resizeCanvas();
const cx = containerRef.current.clientWidth / 2;
const cy = containerRef.current.clientHeight / 2; const handleResize = () => {
mousePos.current = { x: cx, y: cy }; isMobileRef.current = isMobileDevice();
smoothMouse.current = { x: cx, y: cy }; resizeCanvas();
mobileAnchor.current = { x: cx, y: cy };
mobileTarget.current = {
x: rand(cx * 0.4, cx * 1.6),
y: rand(cy * 0.4, cy * 1.6),
}; };
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [resizeCanvas]);
useEffect(() => {
if (!mounted) {
return;
} }
}, []);
/* Resize canvas to match container */ seedPositions();
useEffect(() => { }, [mounted, seedPositions]);
const resize = () => {
const canvas = canvasRef.current; const handleMouseMove = useCallback((event: MouseEvent) => {
const container = containerRef.current; const container = containerRef.current;
if (!canvas || !container) return; if (!container || isMobileRef.current) {
const dpr = window.devicePixelRatio || 1; return;
const w = container.clientWidth; }
const h = container.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
const ctx = canvas.getContext("2d");
if (ctx) ctx.scale(dpr, dpr);
};
resize();
window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize);
}, []);
/* Mouse tracking (desktop only) */ const rect = container.getBoundingClientRect();
const handleMouseMove = useCallback((e: MouseEvent) => { mousePosRef.current = {
if (!containerRef.current || isMobile.current) return; x: event.clientX - rect.left,
const rect = containerRef.current.getBoundingClientRect(); y: event.clientY - rect.top,
mousePos.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
const el = containerRef.current; const container = containerRef.current;
if (!el) return; if (!container) {
el.addEventListener("mousemove", handleMouseMove, { passive: true }); return;
return () => el.removeEventListener("mousemove", handleMouseMove); }
container.addEventListener("mousemove", handleMouseMove, { passive: true });
return () => container.removeEventListener("mousemove", handleMouseMove);
}, [handleMouseMove]); }, [handleMouseMove]);
/* ── GSAP ticker — draw loop ── */ useEffect(() => {
useGSAP( if (!mounted) {
() => { return;
if (!mounted) return; }
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
if (!canvas || !container) return; if (!canvas || !container) {
return;
}
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) {
return;
}
const tick = () => { const draw = () => {
const w = container.clientWidth; const width = container.clientWidth;
const h = container.clientHeight; const height = container.clientHeight;
frame.current++; frameRef.current += 1;
/* Anchor: smooth-follow cursor or drift on mobile */ if (isMobileRef.current) {
if (isMobile.current) { const mobileLerp = 0.008 * mobileSpeedRef.current;
mobileAnchor.current.x += mobileAnchorRef.current.x +=
(mobileTarget.current.x - mobileAnchor.current.x) * 0.008 * mobileSpeed; (mobileTargetRef.current.x - mobileAnchorRef.current.x) * mobileLerp;
mobileAnchor.current.y += mobileAnchorRef.current.y +=
(mobileTarget.current.y - mobileAnchor.current.y) * 0.008 * mobileSpeed; (mobileTargetRef.current.y - mobileAnchorRef.current.y) * mobileLerp;
const dx = mobileTarget.current.x - mobileAnchor.current.x; const dx = mobileTargetRef.current.x - mobileAnchorRef.current.x;
const dy = mobileTarget.current.y - mobileAnchor.current.y; const dy = mobileTargetRef.current.y - mobileAnchorRef.current.y;
if (Math.sqrt(dx * dx + dy * dy) < 30) { if (Math.hypot(dx, dy) < 30) {
mobileTarget.current = { mobileTargetRef.current = {
x: rand(w * 0.15, w * 0.85), x: rand(width * 0.15, width * 0.85),
y: rand(h * 0.15, h * 0.85), y: rand(height * 0.15, height * 0.85),
}; };
} }
smoothMouse.current.x = mobileAnchor.current.x;
smoothMouse.current.y = mobileAnchor.current.y; smoothMouseRef.current = { ...mobileAnchorRef.current };
} else { } else {
smoothMouse.current.x += const desktopLerp = followSpeedRef.current;
(mousePos.current.x - smoothMouse.current.x) * followSpeed; smoothMouseRef.current.x +=
smoothMouse.current.y += (mousePosRef.current.x - smoothMouseRef.current.x) * desktopLerp;
(mousePos.current.y - smoothMouse.current.y) * followSpeed; smoothMouseRef.current.y +=
(mousePosRef.current.y - smoothMouseRef.current.y) * desktopLerp;
} }
const cx = smoothMouse.current.x; const centerX = smoothMouseRef.current.x;
const cy = smoothMouse.current.y; const centerY = smoothMouseRef.current.y;
const colors = particleColorsRef.current;
/* Clear frame */ ctx.clearRect(0, 0, width, height);
ctx.clearRect(0, 0, w, h);
/* Draw each particle */ for (const particle of particlesRef.current) {
particles.current.forEach((p) => { particle.angle += particle.speed;
p.angle += p.speed;
const wobble = const wobble =
Math.sin(frame.current * p.wobbleSpeed + p.wobblePhase) * p.wobbleAmp; Math.sin(frameRef.current * particle.wobbleSpeed + particle.wobblePhase) *
const r = p.radius + wobble; particle.wobbleAmp;
const radius = particle.radius + wobble;
const x = centerX + Math.cos(particle.angle) * radius;
const y = centerY + Math.sin(particle.angle) * radius;
const x = cx + Math.cos(p.angle) * r;
const y = cy + Math.sin(p.angle) * r;
/* Soft fade near viewport edges */
const edgeFade = Math.max( const edgeFade = Math.max(
0, 0,
Math.min(x / 80, (w - x) / 80, y / 80, (h - y) / 80, 1), Math.min(x / 80, (width - x) / 80, y / 80, (height - y) / 80, 1),
); );
if (edgeFade <= 0) return;
if (edgeFade <= 0) {
continue;
}
ctx.globalAlpha = edgeFade; ctx.globalAlpha = edgeFade;
ctx.fillStyle = palette.particles[p.colorIndex]; ctx.fillStyle = colors[particle.colorIndex] ?? colors[0] ?? "#ffffff";
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, p.size, 0, Math.PI * 2); ctx.arc(x, y, particle.size, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
}); }
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
animationFrameRef.current = window.requestAnimationFrame(draw);
}; };
gsap.ticker.add(tick); animationFrameRef.current = window.requestAnimationFrame(draw);
return () => { return () => {
gsap.ticker.remove(tick); if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
}; };
}, }, [mounted]);
{
scope: containerRef,
dependencies: [mounted, isDark, followSpeed, mobileSpeed, orbitRadius, palette],
},
);
/* ── Render ── */
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@@ -281,14 +326,13 @@ export default function AnimatedBackgroundContainer({
}} }}
/> />
{/* Grain texture */}
<div <div
aria-hidden aria-hidden
style={{ style={{
position: "absolute", position: "absolute",
inset: 0, inset: 0,
zIndex: 1, zIndex: 1,
opacity: isDark ? 0.05 : 0.03, opacity: palette.grainOpacity,
pointerEvents: "none", pointerEvents: "none",
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`, backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
backgroundRepeat: "repeat", backgroundRepeat: "repeat",

View File

@@ -1,11 +1,7 @@
import { isAdmin } from '~/app/actions'
import { redirect } from 'next/navigation'
import { servTrpc } from '~/app/_trpc/ServerClient' import { servTrpc } from '~/app/_trpc/ServerClient'
import SystemPromptForm from './_components/SystemPromptForm' import SystemPromptForm from './_components/SystemPromptForm'
export default async function SystemPromptPage() { export default async function SystemPromptPage() {
if (!(await isAdmin())) redirect('/admin')
const prompt = await servTrpc.chat.getSystemPrompt() const prompt = await servTrpc.chat.getSystemPrompt()
return ( return (

View File

@@ -1,10 +1,14 @@
import { redirect } from "next/navigation";
import { isAdmin } from "~/app/actions";
import { SidebarProvider } from "~/components/ui/sidebar"; import { SidebarProvider } from "~/components/ui/sidebar";
import AdminSideBar from "./_components/AdminSideBar"; import AdminSideBar from "./_components/AdminSideBar";
import { ScrollArea } from "~/components/ui/scroll-area"; import { ScrollArea } from "~/components/ui/scroll-area";
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default function Admin({children}: Readonly<{children: React.ReactNode}>) { export default async function Admin({children}: Readonly<{children: React.ReactNode}>) {
if (!(await isAdmin())) redirect("/");
return ( return (
<> <>
<SidebarProvider> <SidebarProvider>

View File

@@ -1,15 +1,9 @@
'use server'
import { Show } from "@clerk/nextjs";
export default async function AdminPage() { export default async function AdminPage() {
return ( return (
<Show when="signed-in">
<main className="flex min-h-screen flex-col items-center justify-center"> <main className="flex min-h-screen flex-col items-center justify-center">
<div> <div>
hello admin hello admin
</div> </div>
</main> </main>
</Show>
) )
} }

View File

@@ -15,7 +15,7 @@ import {MessagesProvider} from "./_providers/MessagesProvider";
import { CodeHighlightStyle } from "./_components/CodeHighlightSyle"; import { CodeHighlightStyle } from "./_components/CodeHighlightSyle";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import AnimatedBackGroundContainer from "./_components/Animated/AnimatedBackGroundContainer"; import AnimatedBackGroundContainer from "./_components/Animated/AnimatedBackGroundContainer";
import {SpeedInsights} from "@vercel/speed-insights/next"
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const inter = Inter({ subsets: ['latin'], variable: '--font-sans' });
@@ -37,6 +37,8 @@ export default async function RootLayout({
}: Readonly<{ children: React.ReactNode, modal: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode, modal: React.ReactNode }>) {
return ( return (
<>
<SpeedInsights/>
<ClerkProvider> <ClerkProvider>
<TrpcProvider> <TrpcProvider>
<GsapProvider> <GsapProvider>
@@ -62,5 +64,6 @@ export default async function RootLayout({
</GsapProvider> </GsapProvider>
</TrpcProvider> </TrpcProvider>
</ClerkProvider> </ClerkProvider>
</>
); );
} }

View File

@@ -34,7 +34,7 @@ export default function ProjectsPage() {
return ( return (
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10"> <ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
<AnimatedPageTitle position={0}><span>Project I've Been</span><span> Working on</span> </AnimatedPageTitle> <AnimatedPageTitle position={0}><span>Projects I've Been</span><span> Working on</span> </AnimatedPageTitle>
<div className="pt-10" /> <div className="pt-10" />
{projects.map((project, i) => ( {projects.map((project, i) => (
<div id={project.id} key={i} className="scroll-mt-10"> <div id={project.id} key={i} className="scroll-mt-10">

View File

@@ -1,13 +1,16 @@
import { clerkMiddleware, createRouteMatcher, currentUser } from "@clerk/nextjs/server"; import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { env } from "~/env"; import { env } from "~/env";
const isTenantAdminRoute = createRouteMatcher(['/admin(.*)']) const isTenantAdminRoute = createRouteMatcher(["/admin(.*)"]);
export default clerkMiddleware(async (auth,req) => {
export default clerkMiddleware(async (auth, req) => {
if (isTenantAdminRoute(req)) { if (isTenantAdminRoute(req)) {
console.log("running clerk middleware"); await auth.protect();
let userid = (await auth()).userId
if (userid != env.ADMIN_USER_CLERK_ID) { const { userId } = await auth();
await auth.protect() if (userId !== env.ADMIN_USER_CLERK_ID) {
return NextResponse.redirect(new URL("/", req.url));
} }
} }
}); });

View File

@@ -9,12 +9,12 @@ import { eq } from 'drizzle-orm';
import { clerkClient, auth } from '@clerk/nextjs/server' import { clerkClient, auth } from '@clerk/nextjs/server'
export const chatRouter = router({ export const chatRouter = router({
getSession: publicProcedure.query(async () => { getSession: publicProcedure.query(async () => {
const clerk = await clerkClient()
const { userId } = await auth(); const { userId } = await auth();
const user = await clerk.users.getUser(userId?userId:"") if (!userId) {
if (user == undefined) {
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' }); throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
} }
const clerk = await clerkClient()
const user = await clerk.users.getUser(userId)
let session = await db.query.chatSession.findFirst({ let session = await db.query.chatSession.findFirst({
where(fields, operators) { where(fields, operators) {
return operators.eq(fields.userId, user.id) return operators.eq(fields.userId, user.id)