Compare commits
5 Commits
538d896b0e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 65b9184a22 | |||
| ea7ddb8e51 | |||
| bcefe397ca | |||
| 8ce95f2b5c | |||
| da43b31aa3 |
3
bun.lock
3
bun.lock
@@ -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=="],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 (0–1). 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",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
15
src/proxy.ts
15
src/proxy.ts
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user