package updates, minor bug fixes, stack items, background animation fixes
This commit is contained in:
2
.ignore
2
.ignore
@@ -1,2 +1,4 @@
|
|||||||
node_modules/**
|
node_modules/**
|
||||||
.next/**
|
.next/**
|
||||||
|
.worktrees
|
||||||
|
.clerk
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
96
package.json
96
package.json
@@ -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 |
@@ -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
|
||||||
|
|||||||
53
src/app/_components/Form/Fields/IntInputFormField.tsx
Normal file
53
src/app/_components/Form/Fields/IntInputFormField.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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[] }) {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
:
|
:
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -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> :
|
||||||
<></>
|
<></>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user