package updates, minor bug fixes, stack items, background animation fixes

This commit is contained in:
2026-06-06 12:15:01 +02:00
parent 65b9184a22
commit f5e8b87846
21 changed files with 775 additions and 359 deletions

View File

@@ -1,2 +1,4 @@
node_modules/** node_modules/**
.next/** .next/**
.worktrees
.clerk

676
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,10 @@ import "./src/env.js";
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
transpilePackages: ["next-mdx-remote"],
typescript: { typescript: {
ignoreBuildErrors: true ignoreBuildErrors: true,
} },
}; };
export default config; export default config;

View File

@@ -19,16 +19,16 @@
"test": "vitest --typecheck" "test": "vitest --typecheck"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^3.0.41", "@ai-sdk/openai": "^3.0.67",
"@ai-sdk/react": "^3.0.118", "@ai-sdk/react": "^3.0.195",
"@clerk/nextjs": "^7.0.2", "@clerk/nextjs": "^7.4.2",
"@electric-sql/pglite": "^0.3.16", "@electric-sql/pglite": "^0.4.6",
"@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.2.0", "@fortawesome/react-fontawesome": "^3.3.1",
"@gsap/react": "^2.1.2", "@gsap/react": "^2.1.2",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.4.0",
"@neondatabase/serverless": "^1.0.2", "@neondatabase/serverless": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -55,85 +55,85 @@
"@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.100.14",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.100.14",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@trpc/client": "^11.12.0", "@trpc/client": "^11.17.0",
"@trpc/next": "^11.12.0", "@trpc/next": "^11.17.0",
"@trpc/react-query": "^11.12.0", "@trpc/react-query": "^11.17.0",
"@trpc/server": "^11.12.0", "@trpc/server": "^11.17.0",
"@uiw/react-md-editor": "^4.0.11", "@uiw/react-md-editor": "^4.1.1",
"@uploadthing/react": "^7.3.3", "@uploadthing/react": "^7.3.3",
"@vercel/speed-insights": "^2.0.0", "@vercel/speed-insights": "^2.0.0",
"ai": "^6.0.116", "ai": "^6.0.193",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.4.0",
"date-format": "^4.0.14", "date-format": "^4.0.14",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.2",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"glazejs": "^2.0.1", "glazejs": "^2.0.1",
"googleapis": "^171.4.0", "googleapis": "^173.0.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"gsap": "^3.14.2", "gsap": "^3.15.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.577.0", "lucide-react": "^1.17.0",
"next": "16.1.6", "next": "16.2.6",
"next-mdx-remote": "^6.0.0", "next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"postgres": "^3.4.8", "postgres": "^3.4.9",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.4", "react": "^19.2.6",
"react-day-picker": "^9.14.0", "react-day-picker": "^10.0.1",
"react-dom": "^19.2.4", "react-dom": "^19.2.6",
"react-hook-form": "^7.71.2", "react-hook-form": "^7.77.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^4.7.2", "react-resizable-panels": "^4.11.2",
"recharts": "2.15.4", "recharts": "3.8.1",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"shadcn": "^4.0.2", "shadcn": "^4.10.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.6.0",
"tailwindcss-motion": "^1.1.1", "tailwindcss-motion": "^1.1.1",
"type-fest": "^5.4.4", "type-fest": "^5.7.0",
"uploadthing": "^7.7.4", "uploadthing": "^7.7.4",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^4.3.6" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.6", "@biomejs/biome": "2.4.16",
"@swc/jest": "^0.2.39", "@swc/jest": "^0.2.39",
"@tailwindcss/postcss": "^4.2.1", "@tailwindcss/postcss": "^4.3.0",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.4.0", "@types/node": "^25.9.1",
"@types/react": "^19.2.14", "@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4", "@vitejs/plugin-react": "^6.0.2",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.1.8",
"dotenv": "^17.3.1", "dotenv": "^17.4.2",
"drizzle-kit": "^0.31.9", "drizzle-kit": "^0.31.10",
"jest": "^30.3.0", "jest": "^30.4.2",
"jest-environment-jsdom": "^30.3.0", "jest-environment-jsdom": "^30.4.1",
"jsdom": "^28.1.0", "jsdom": "^29.1.1",
"next-router-mock": "^1.0.5", "next-router-mock": "^1.0.5",
"pg-mem": "^3.0.14", "pg-mem": "^3.0.14",
"postcss": "^8.5.8", "postcss": "^8.5.15",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.3.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "typescript": "^6.0.3",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18" "vitest": "^4.1.8"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.39.3" "initVersion": "7.39.3"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +1,8 @@
"use client"; "use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import type React from "react";
import { useCallback, useEffect, useRef } from "react";
const PALETTES = { const PALETTES = {
dark: { dark: {
@@ -37,6 +38,12 @@ interface Particle {
wobblePhase: number; wobblePhase: number;
} }
interface CanvasSize {
dpr: number;
height: number;
width: number;
}
interface AnimatedBackgroundContainerProps { interface AnimatedBackgroundContainerProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
@@ -48,6 +55,9 @@ interface AnimatedBackgroundContainerProps {
const DEFAULT_PARTICLE_COLORS: readonly string[] = PALETTES.dark.particles; const DEFAULT_PARTICLE_COLORS: readonly string[] = PALETTES.dark.particles;
const PARTICLE_COLOR_COUNT = DEFAULT_PARTICLE_COLORS.length; const PARTICLE_COLOR_COUNT = DEFAULT_PARTICLE_COLORS.length;
const EDGE_FADE_DISTANCE = 80;
const MAX_DEVICE_PIXEL_RATIO = 2;
const MOBILE_TARGET_DISTANCE = 30;
const rand = (min: number, max: number) => Math.random() * (max - min) + min; const rand = (min: number, max: number) => Math.random() * (max - min) + min;
@@ -87,15 +97,18 @@ export default function AnimatedBackgroundContainer({
const frameRef = useRef(0); const frameRef = useRef(0);
const animationFrameRef = useRef<number | null>(null); const animationFrameRef = useRef<number | null>(null);
const isMobileRef = useRef(false); const isMobileRef = useRef(false);
const isVisibleRef = useRef(true);
const prefersReducedMotionRef = useRef(false);
const particlesRef = useRef<Particle[]>([]); const particlesRef = useRef<Particle[]>([]);
const mousePosRef = useRef({ x: 0, y: 0 }); const mousePosRef = useRef({ x: 0, y: 0 });
const smoothMouseRef = useRef({ x: 0, y: 0 }); const smoothMouseRef = useRef({ x: 0, y: 0 });
const mobileAnchorRef = useRef({ x: 0, y: 0 }); const mobileAnchorRef = useRef({ x: 0, y: 0 });
const mobileTargetRef = useRef({ x: 0, y: 0 }); const mobileTargetRef = useRef({ x: 0, y: 0 });
const canvasSizeRef = useRef<CanvasSize>({ dpr: 1, height: 0, width: 0 });
const containerRectRef = useRef<DOMRect | null>(null);
const followSpeedRef = useRef(followSpeed); const followSpeedRef = useRef(followSpeed);
const mobileSpeedRef = useRef(mobileSpeed); const mobileSpeedRef = useRef(mobileSpeed);
const particleColorsRef = useRef<readonly string[]>(DEFAULT_PARTICLE_COLORS); const particleColorsRef = useRef<readonly string[]>(DEFAULT_PARTICLE_COLORS);
const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === undefined || resolvedTheme === "dark"; const isDark = resolvedTheme === undefined || resolvedTheme === "dark";
@@ -120,13 +133,13 @@ export default function AnimatedBackgroundContainer({
}, [particleCount, orbitRadius]); }, [particleCount, orbitRadius]);
const seedPositions = useCallback(() => { const seedPositions = useCallback(() => {
const container = containerRef.current; const { height, width } = canvasSizeRef.current;
if (!container) { if (width === 0 || height === 0) {
return; return;
} }
const centerX = container.clientWidth / 2; const centerX = width / 2;
const centerY = container.clientHeight / 2; const centerY = height / 2;
mousePosRef.current = { x: centerX, y: centerY }; mousePosRef.current = { x: centerX, y: centerY };
smoothMouseRef.current = { x: centerX, y: centerY }; smoothMouseRef.current = { x: centerX, y: centerY };
@@ -137,6 +150,13 @@ export default function AnimatedBackgroundContainer({
}; };
}, []); }, []);
const updateContainerRect = useCallback(() => {
const container = containerRef.current;
if (container) {
containerRectRef.current = container.getBoundingClientRect();
}
}, []);
const resizeCanvas = useCallback(() => { const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
@@ -146,10 +166,19 @@ export default function AnimatedBackgroundContainer({
const width = container.clientWidth; const width = container.clientWidth;
const height = container.clientHeight; const height = container.clientHeight;
const dpr = window.devicePixelRatio || 1; const dpr = Math.min(window.devicePixelRatio || 1, MAX_DEVICE_PIXEL_RATIO);
const nextWidth = Math.round(width * dpr);
const nextHeight = Math.round(height * dpr);
canvas.width = width * dpr; canvasSizeRef.current = { dpr, height, width };
canvas.height = height * dpr; updateContainerRect();
if (canvas.width !== nextWidth) {
canvas.width = nextWidth;
}
if (canvas.height !== nextHeight) {
canvas.height = nextHeight;
}
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${height}px`;
@@ -160,14 +189,10 @@ export default function AnimatedBackgroundContainer({
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr); ctx.scale(dpr, dpr);
if (!mounted) {
seedPositions(); seedPositions();
} }, [seedPositions, updateContainerRect]);
}, [mounted, seedPositions]);
useEffect(() => { useEffect(() => {
setMounted(true);
isMobileRef.current = isMobileDevice(); isMobileRef.current = isMobileDevice();
resizeCanvas(); resizeCanvas();
@@ -176,25 +201,35 @@ export default function AnimatedBackgroundContainer({
resizeCanvas(); resizeCanvas();
}; };
window.addEventListener("resize", handleResize); const resizeObserver =
return () => window.removeEventListener("resize", handleResize); "ResizeObserver" in window
}, [resizeCanvas]); ? new ResizeObserver(() => {
handleResize();
})
: null;
useEffect(() => { if (containerRef.current && resizeObserver) {
if (!mounted) { resizeObserver.observe(containerRef.current);
return;
} }
seedPositions(); window.addEventListener("resize", handleResize);
}, [mounted, seedPositions]); window.addEventListener("scroll", updateContainerRect, {
capture: true,
passive: true,
});
return () => {
resizeObserver?.disconnect();
window.removeEventListener("resize", handleResize);
window.removeEventListener("scroll", updateContainerRect, { capture: true });
};
}, [resizeCanvas, updateContainerRect]);
const handleMouseMove = useCallback((event: MouseEvent) => { const handleMouseMove = useCallback((event: MouseEvent) => {
const container = containerRef.current; const rect = containerRectRef.current;
if (!container || isMobileRef.current) { if (!rect || isMobileRef.current) {
return; return;
} }
const rect = container.getBoundingClientRect();
mousePosRef.current = { mousePosRef.current = {
x: event.clientX - rect.left, x: event.clientX - rect.left,
y: event.clientY - rect.top, y: event.clientY - rect.top,
@@ -212,13 +247,8 @@ export default function AnimatedBackgroundContainer({
}, [handleMouseMove]); }, [handleMouseMove]);
useEffect(() => { useEffect(() => {
if (!mounted) {
return;
}
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; if (!canvas) {
if (!canvas || !container) {
return; return;
} }
@@ -227,9 +257,25 @@ export default function AnimatedBackgroundContainer({
return; return;
} }
const stopAnimation = () => {
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
};
const draw = () => { const draw = () => {
const width = container.clientWidth; if (!isVisibleRef.current || prefersReducedMotionRef.current) {
const height = container.clientHeight; animationFrameRef.current = null;
return;
}
const { height, width } = canvasSizeRef.current;
if (width === 0 || height === 0) {
animationFrameRef.current = window.requestAnimationFrame(draw);
return;
}
frameRef.current += 1; frameRef.current += 1;
if (isMobileRef.current) { if (isMobileRef.current) {
@@ -241,14 +287,15 @@ export default function AnimatedBackgroundContainer({
const dx = mobileTargetRef.current.x - mobileAnchorRef.current.x; const dx = mobileTargetRef.current.x - mobileAnchorRef.current.x;
const dy = mobileTargetRef.current.y - mobileAnchorRef.current.y; const dy = mobileTargetRef.current.y - mobileAnchorRef.current.y;
if (Math.hypot(dx, dy) < 30) { if (Math.hypot(dx, dy) < MOBILE_TARGET_DISTANCE) {
mobileTargetRef.current = { mobileTargetRef.current = {
x: rand(width * 0.15, width * 0.85), x: rand(width * 0.15, width * 0.85),
y: rand(height * 0.15, height * 0.85), y: rand(height * 0.15, height * 0.85),
}; };
} }
smoothMouseRef.current = { ...mobileAnchorRef.current }; smoothMouseRef.current.x = mobileAnchorRef.current.x;
smoothMouseRef.current.y = mobileAnchorRef.current.y;
} else { } else {
const desktopLerp = followSpeedRef.current; const desktopLerp = followSpeedRef.current;
smoothMouseRef.current.x += smoothMouseRef.current.x +=
@@ -275,7 +322,13 @@ export default function AnimatedBackgroundContainer({
const edgeFade = Math.max( const edgeFade = Math.max(
0, 0,
Math.min(x / 80, (width - x) / 80, y / 80, (height - y) / 80, 1), Math.min(
x / EDGE_FADE_DISTANCE,
(width - x) / EDGE_FADE_DISTANCE,
y / EDGE_FADE_DISTANCE,
(height - y) / EDGE_FADE_DISTANCE,
1,
),
); );
if (edgeFade <= 0) { if (edgeFade <= 0) {
@@ -293,15 +346,51 @@ export default function AnimatedBackgroundContainer({
animationFrameRef.current = window.requestAnimationFrame(draw); animationFrameRef.current = window.requestAnimationFrame(draw);
}; };
const startAnimation = () => {
if (
animationFrameRef.current === null &&
isVisibleRef.current &&
!prefersReducedMotionRef.current
) {
animationFrameRef.current = window.requestAnimationFrame(draw); animationFrameRef.current = window.requestAnimationFrame(draw);
return () => {
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
} }
}; };
}, [mounted]);
const handleVisibilityChange = () => {
isVisibleRef.current = document.visibilityState === "visible";
if (isVisibleRef.current) {
startAnimation();
} else {
stopAnimation();
}
};
const motionMedia = window.matchMedia("(prefers-reduced-motion: reduce)");
const handleMotionChange = () => {
prefersReducedMotionRef.current = motionMedia.matches;
if (prefersReducedMotionRef.current) {
stopAnimation();
ctx.clearRect(0, 0, canvasSizeRef.current.width, canvasSizeRef.current.height);
} else {
startAnimation();
}
};
isVisibleRef.current = document.visibilityState === "visible";
prefersReducedMotionRef.current = motionMedia.matches;
startAnimation();
document.addEventListener("visibilitychange", handleVisibilityChange);
motionMedia.addEventListener("change", handleMotionChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
motionMedia.removeEventListener("change", handleMotionChange);
stopAnimation();
};
}, []);
return ( return (
<div <div

View File

@@ -0,0 +1,53 @@
import type { ComponentProps } from "react";
import type { Control, FieldValues, Path } from "react-hook-form";
import { FormControl, FormField, FormItem, FormLabel } from "~/components/ui/form";
import { Input } from "~/components/ui/input";
type IntInputFormFieldProps<T extends FieldValues> = Omit<
ComponentProps<typeof Input>,
"defaultValue" | "name" | "onChange" | "type" | "value"
> & {
control: Control<T>;
emptyValue?: null | undefined;
label: string;
name: Path<T>;
};
export default function IntInputFormField<T extends FieldValues>({
control,
emptyValue,
label,
name,
...inputProps
}: IntInputFormFieldProps<T>) {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>{label}</FormLabel>
<FormControl>
<Input
{...inputProps}
inputMode="numeric"
onBlur={field.onBlur}
onChange={(event) => {
const value = event.currentTarget.value;
field.onChange(
value === "" ? emptyValue : Number.parseInt(value, 10),
);
}}
placeholder={inputProps.placeholder ?? name}
ref={field.ref}
step={inputProps.step ?? 1}
type="number"
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
)}
/>
);
}

View File

@@ -1,6 +1,7 @@
import type { CheckedState } from "@radix-ui/react-checkbox"; import type { CheckedState } from "@radix-ui/react-checkbox";
import { ChevronDownIcon } from "lucide-react"; import { ChevronDownIcon } from "lucide-react";
import { createContext,useContext, useState } from "react"; import { ScrollArea } from "~/components/ui/scroll-area";
import { createContext,useContext, useState, type KeyboardEventHandler } from "react";
import { useFormContext, type Control, type ControllerRenderProps, type FieldValues, type Path } from "react-hook-form"; import { useFormContext, type Control, type ControllerRenderProps, type FieldValues, type Path } from "react-hook-form";
import type { Entries } from "type-fest"; import type { Entries } from "type-fest";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@@ -16,6 +17,7 @@ const MultiBooleanFieldContext = createContext<MultiBooleanFieldContextProps|und
function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0: string[]) => void }) { function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0: string[]) => void }) {
const context = useContext(MultiBooleanFieldContext) const context = useContext(MultiBooleanFieldContext)
const [searchBuffer, setSearchBuffer] = useState<string>("")
if (context === undefined) { if (context === undefined) {
return (<></>) return (<></>)
} }
@@ -43,19 +45,40 @@ function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0
} }
return context.checkedValues[key] return context.checkedValues[key]
} }
const handleKeyDown:KeyboardEventHandler = (e) => {
if (e.ctrlKey && e.key == "c") {
setSearchBuffer("")
} else if (e.code == "Backspace") {
setSearchBuffer((prev) => {
const newVal = prev.substring(0,prev.length - 2)
console.log(newVal)
return newVal
})
}
else if (e.key.length === 1) {
setSearchBuffer((prev) => {
const newVal = prev + e.key;
console.log(newVal)
return newVal;
})
}
}
return ( return (
<> <ScrollArea onKeyDown={handleKeyDown} className="flex h-60">
{ {
params.options.map((opt) => ( params.options.map((opt) => (
<FormItem key={opt}> opt.startsWith(searchBuffer) && <FormItem key={opt}>
<div className="flex flex-row justify-between py-2 border-b-1"> <div className="flex flex-row justify-between py-2 border-b">
<FormLabel>{opt}</FormLabel> <FormLabel>{opt}</FormLabel>
<Checkbox data-testid="multiboolean-checkbox" checked={checked(opt)} onCheckedChange={onCheckedItemChange(opt)} /> <Checkbox data-testid="multiboolean-checkbox" checked={checked(opt)} onCheckedChange={onCheckedItemChange(opt)} />
</div> </div>
</FormItem> </FormItem>
)) ))
} }
</> </ScrollArea>
) )
} }
export default function MultiBooleanFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, options: string[], defaultValues?: string[] }) { export default function MultiBooleanFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, options: string[], defaultValues?: string[] }) {

View File

@@ -1,5 +1,6 @@
export { default as BooleanFormField } from './BooleanFormField' export { default as BooleanFormField } from './BooleanFormField'
export { default as TextInputFormField } from './TextInputFormField' export { default as TextInputFormField } from './TextInputFormField'
export { default as IntInputFormField } from './IntInputFormField'
export { default as MultiBooleanFormField } from './MultiBooleanFormField' export { default as MultiBooleanFormField } from './MultiBooleanFormField'
export { default as SelectFormField } from './SelectFormField' export { default as SelectFormField } from './SelectFormField'
export { default as MdeFormField } from './MdeFormField' export { default as MdeFormField } from './MdeFormField'

View File

@@ -3,7 +3,7 @@ import type { inferRouterOutputs } from '@trpc/server';
import { useUser } from '@clerk/nextjs' import { useUser } from '@clerk/nextjs'
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { trpc } from '~/app/_trpc/Client' import { trpc } from '~/app/_trpc/Client'
import { type ChatRouter } from '~/server/routers/chat' import type { ChatRouter } from '~/server/routers/chat'
const MessageContext = createContext<{ const MessageContext = createContext<{
session?: inferRouterOutputs<ChatRouter>['getSession'] session?: inferRouterOutputs<ChatRouter>['getSession']
messages?: inferRouterOutputs<ChatRouter>['getMessages'] messages?: inferRouterOutputs<ChatRouter>['getMessages']

View File

@@ -6,8 +6,8 @@ import SimpleSidebarGroup from "~/components/ui/simple-sidebar-group";
export default function AdminSideBar() { export default function AdminSideBar() {
return ( return (
<> <>
<Sidebar variant="floating" className="h-[96%] mt-10 z-[51]"> <Sidebar variant="floating" className="h-full lg:h-[96%] lg:mt-10 z-51">
<SidebarTrigger className="absolute z-[52] left-65 top-100" /> <SidebarTrigger className="absolute z-52 left-65 top-100" />
<SidebarContent> <SidebarContent>
<ScrollArea> <ScrollArea>
<SimpleSidebarGroup lable="CV"> <SimpleSidebarGroup lable="CV">

View File

@@ -8,7 +8,7 @@ import type { IterableElement } from 'type-fest'
import { entitySchemas, makeOnSuccess } from "~/lib/utils"; import { entitySchemas, makeOnSuccess } from "~/lib/utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { RouterOutputs } from '~/server/routers/_app'; import type { RouterOutputs } from '~/server/routers/_app';
import { SelectFormField, TextInputFormField, MdeFormField } from '~/app/_components/Form/Fields' import { SelectFormField, TextInputFormField, MdeFormField, IntInputFormField } from '~/app/_components/Form/Fields'
import { FormScaffold } from '~/app/_components/Form/Components'; import { FormScaffold } from '~/app/_components/Form/Components';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
@@ -29,7 +29,8 @@ export default function CreateUpdateProjectForm(params: { className?: string, en
releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased", releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased",
releaseLink: params.entity ? params.entity.releaseLink : "", releaseLink: params.entity ? params.entity.releaseLink : "",
sourceType: params.entity ? params.entity.sourceType : "open", sourceType: params.entity ? params.entity.sourceType : "open",
sourceLink: params.entity ? params.entity.sourceLink : "" sourceLink: params.entity ? params.entity.sourceLink : "",
orderPos: params.entity ? params.entity.orderPos : 0
} }
}) })
let path = usePathname() let path = usePathname()
@@ -79,6 +80,7 @@ export default function CreateUpdateProjectForm(params: { className?: string, en
<SelectItem value="unreleased"> unreleased </SelectItem> <SelectItem value="unreleased"> unreleased </SelectItem>
</SelectFormField> </SelectFormField>
<TextInputFormField control={form.control} label='Release Link' name='releaseLink' /> <TextInputFormField control={form.control} label='Release Link' name='releaseLink' />
<IntInputFormField control={form.control} label='Order Position' name='orderPos'/>
</FormScaffold> </FormScaffold>
</FormMutationContextProvider> </FormMutationContextProvider>
) )

View File

@@ -46,7 +46,7 @@ export default function CreateUpdateStackForm(params: { className?: string, enti
id={id} id={id}
className={params.className} className={params.className}
> >
<MultiBooleanFormField control={form.control} name='stackItems' label='Stack Items' options={stackItemEnum.enumValues} defaultValues={params.entity?.stackItems ?? [""]} /> <MultiBooleanFormField control={form.control} name='stackItems' label='Stack Items' options={stackItemEnum.enumValues} defaultValues={params.entity?.stackItems ?? []} />
</FormScaffold> </FormScaffold>
</FormMutationContextProvider> </FormMutationContextProvider>
</> </>

View File

@@ -16,7 +16,7 @@ export const AssistantMessage = (props: { message: UIMessage }) => {
{message.parts.map((part, i) => { {message.parts.map((part, i) => {
if (part.type === 'text') { if (part.type === 'text') {
return ( return (
<Markdown> <Markdown key={crypto.randomUUID()}>
{part.text} {part.text}
</Markdown> </Markdown>
) )

View File

@@ -7,6 +7,8 @@ import type { RouterOutputs } from "~/server/routers/_app"
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import type { ArrayElement } from "type-fest" import type { ArrayElement } from "type-fest"
import AnimatePopUp from "~/app/_components/Animated/AnimatePopUp";
import { ScrollArea } from "~/components/ui/scroll-area";
type CvCategoryProps = { type CvCategoryProps = {
initialData: ArrayElement<RouterOutputs['categoryv2']['listByLayoutPosition']>, initialData: ArrayElement<RouterOutputs['categoryv2']['listByLayoutPosition']>,
layout: "row"|"col", layout: "row"|"col",
@@ -14,18 +16,23 @@ type CvCategoryProps = {
} }
export default function CvCategory(props:CvCategoryProps) { export default function CvCategory(props:CvCategoryProps) {
const category = trpc.categoryv2.getById.useQuery(props.initialData? props.initialData.id : ""); const category = trpc.categoryv2.getById.useQuery(props.initialData? props.initialData.id : "");
const entries = trpc.entryv2.byCategoryAndToDateDescending.useQuery(category.data?.id || "")
return ( return (
<Card className={cn(props.layout == "row" ? "w-full" : "","gsapan")}> <Card className={cn(props.layout == "row" ? "w-full" : "","gsapan", "h-screen")}>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
{category.data?.name} {category.data?.name}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
{(category.data?.cvEntry.length ? category.data?.cvEntry.length : 0 ) > 0 ? {(entries.data?.length ? entries.data?.length : 0 ) > 0 ?
<CardContent className={cn(props.layout == "row" ? "flex flex-row flex-wrap justify-center lg:justify-between" : "flex flex-col","gap-4","overflow-scroll")}> <CardContent className={cn(props.layout == "row" ? "flex flex-row flex-wrap justify-center lg:justify-between" : "flex flex-col","gap-4","overflow-scroll")}>
{category.data?.cvEntry.map((entry) => ( <ScrollArea>
<CvEntry className={props.layout == "row" ? "w-full lg:w-fit" : undefined} key={entry.id} initialData={entry}/> {entries.data?.map((entry,i) => (
<AnimatePopUp position={i}key={entry.id}>
<CvEntry className={props.layout == "row" ? "w-full lg:w-fit" : undefined} initialData={entry}/>
</AnimatePopUp>
))} ))}
</ScrollArea>
</CardContent> </CardContent>
: :
<></> <></>

View File

@@ -30,9 +30,9 @@ export default function CvEntry(params: {
{ {
data.description ? data.description ?
<CardContent className="text-sm lg:text-base"> <CardContent className="text-sm lg:text-base">
<div> <article className="prose prose-zinc dark:prose-invert max-w-none">
<Markdown rehypePlugins={[rehypeHighlight, rehypeRaw]}>{data.description}</Markdown> <Markdown rehypePlugins={[rehypeHighlight, rehypeRaw]}>{data.description}</Markdown>
</div> </article>
</CardContent> : </CardContent> :
<></> <></>
} }

View File

@@ -125,6 +125,30 @@ const STACK_META: Record<string, { label: string; icon?: SvglIcon }> = {
dark: "https://svgl.app/library/neon.svg", dark: "https://svgl.app/library/neon.svg",
}, },
}, },
typescript: {
label: "TypeScript",
icon: {
light: "https://svgl.app/library/typescript.svg",
dark: "https://svgl.app/library/typescript.svg",
},
},
javafx: { label: "JavaFX" },
x11: { label: "X11" },
zig: {
label: "Zig",
icon: {
light: "https://svgl.app/library/zig.svg",
dark: "https://svgl.app/library/zig.svg",
},
},
libghostty: { label: "libghostty" },
zod: {
label: "Zod",
icon: {
light: "https://svgl.app/library/zod.svg",
dark: "https://svgl.app/library/zod.svg",
},
},
}; };
export function StackBadge({ item }: { item: string }) { export function StackBadge({ item }: { item: string }) {

View File

@@ -43,6 +43,7 @@ export const cvEntry = createTable(
.default(sql`CURRENT_TIMESTAMP`) .default(sql`CURRENT_TIMESTAMP`)
.notNull().$type<Date>(), .notNull().$type<Date>(),
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(), updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(),
position: d.integer(),
}) })
) )
@@ -55,7 +56,7 @@ export const cvEntryRelations = relations(cvEntry, ({one}) => ({
export const sourceTypeEnum = pgEnum('source_type',['open','closed']) export const sourceTypeEnum = pgEnum('source_type',['open','closed'])
export const releaseStatus = pgEnum('release_status',['released','unreleased']) export const releaseStatus = pgEnum('release_status',['released','unreleased'])
export const stackItemEnum = pgEnum('stack_item',['drizzle','postgres','nextjs','react','servercomponents','php','laravel','reactnative','expo','mysql','nginx','protobuf','grpc','java','graalvm','spring','aws','s3','react-native','linux','debian','htmx','neon']) export const stackItemEnum = pgEnum('stack_item',['drizzle','postgres','nextjs','react','servercomponents','php','laravel','reactnative','expo','mysql','nginx','protobuf','grpc','java','graalvm','spring','aws','s3','react-native','linux','debian','htmx','neon','typescript','javafx','x11','zig','libghostty','zod'])
export const project = createTable( export const project = createTable(
"project", "project",
@@ -68,6 +69,7 @@ export const project = createTable(
releaseStatus: releaseStatus(), releaseStatus: releaseStatus(),
releaseLink: d.varchar({length: 200}), releaseLink: d.varchar({length: 200}),
stackId: d.uuid(), stackId: d.uuid(),
orderPos: d.integer(),
}) })
) )

View File

@@ -15,9 +15,6 @@ export const cvCategoryRouter = router({
}), }),
getById: publicProcedure.input(z.string()).query(async ({input}) => { getById: publicProcedure.input(z.string()).query(async ({input}) => {
const res = await db.query.cvCategory.findFirst({ const res = await db.query.cvCategory.findFirst({
with: {
cvEntry: true
},
where(fields, operators) { where(fields, operators) {
return operators.eq(fields.id, input) return operators.eq(fields.id, input)
}, },

View File

@@ -13,4 +13,14 @@ export const cvEntryRouter = router({
}) })
return res; return res;
}), }),
byCategoryAndToDateDescending: publicProcedure.input(z.string()).query(async ({input}) => {
const res = await db.query.cvEntry.findMany({
with: {
category: true
},
where: (fields, {eq}) => eq(fields.categoryId,input),
orderBy: (t,{desc}) => desc(t.toTime),
})
return res;
})
}); });

View File

@@ -1,5 +1,5 @@
import { publicProcedure, router } from "~/server/trpc";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { publicProcedure, router } from "~/server/trpc";
type ReadmeRequest = { type ReadmeRequest = {
url: string; url: string;
@@ -71,6 +71,11 @@ async function fetchReadme(sourceLink: string) {
export const projectRouter = router({ export const projectRouter = router({
listWithStack: publicProcedure.query(async () => { listWithStack: publicProcedure.query(async () => {
const projects = await db.query.project.findMany({ const projects = await db.query.project.findMany({
orderBy: (project, { asc }) => [
asc(project.orderPos),
asc(project.title),
asc(project.id),
],
with: { with: {
techStack: true, techStack: true,
}, },