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/**
.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} */
const config = {
typescript: {
ignoreBuildErrors: true
}
transpilePackages: ["next-mdx-remote"],
typescript: {
ignoreBuildErrors: true,
},
};
export default config;

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +1,8 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTheme } from "next-themes";
import type React from "react";
import { useCallback, useEffect, useRef } from "react";
const PALETTES = {
dark: {
@@ -37,6 +38,12 @@ interface Particle {
wobblePhase: number;
}
interface CanvasSize {
dpr: number;
height: number;
width: number;
}
interface AnimatedBackgroundContainerProps {
children: React.ReactNode;
className?: string;
@@ -48,6 +55,9 @@ interface AnimatedBackgroundContainerProps {
const DEFAULT_PARTICLE_COLORS: readonly string[] = PALETTES.dark.particles;
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;
@@ -87,15 +97,18 @@ export default function AnimatedBackgroundContainer({
const frameRef = useRef(0);
const animationFrameRef = useRef<number | null>(null);
const isMobileRef = useRef(false);
const isVisibleRef = useRef(true);
const prefersReducedMotionRef = useRef(false);
const particlesRef = useRef<Particle[]>([]);
const mousePosRef = useRef({ x: 0, y: 0 });
const smoothMouseRef = useRef({ x: 0, y: 0 });
const mobileAnchorRef = 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 mobileSpeedRef = useRef(mobileSpeed);
const particleColorsRef = useRef<readonly string[]>(DEFAULT_PARTICLE_COLORS);
const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === undefined || resolvedTheme === "dark";
@@ -120,13 +133,13 @@ export default function AnimatedBackgroundContainer({
}, [particleCount, orbitRadius]);
const seedPositions = useCallback(() => {
const container = containerRef.current;
if (!container) {
const { height, width } = canvasSizeRef.current;
if (width === 0 || height === 0) {
return;
}
const centerX = container.clientWidth / 2;
const centerY = container.clientHeight / 2;
const centerX = width / 2;
const centerY = height / 2;
mousePosRef.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 canvas = canvasRef.current;
const container = containerRef.current;
@@ -146,10 +166,19 @@ export default function AnimatedBackgroundContainer({
const width = container.clientWidth;
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;
canvas.height = height * dpr;
canvasSizeRef.current = { dpr, height, width };
updateContainerRect();
if (canvas.width !== nextWidth) {
canvas.width = nextWidth;
}
if (canvas.height !== nextHeight) {
canvas.height = nextHeight;
}
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
@@ -160,14 +189,10 @@ export default function AnimatedBackgroundContainer({
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
if (!mounted) {
seedPositions();
}
}, [mounted, seedPositions]);
seedPositions();
}, [seedPositions, updateContainerRect]);
useEffect(() => {
setMounted(true);
isMobileRef.current = isMobileDevice();
resizeCanvas();
@@ -176,25 +201,35 @@ export default function AnimatedBackgroundContainer({
resizeCanvas();
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [resizeCanvas]);
const resizeObserver =
"ResizeObserver" in window
? new ResizeObserver(() => {
handleResize();
})
: null;
useEffect(() => {
if (!mounted) {
return;
if (containerRef.current && resizeObserver) {
resizeObserver.observe(containerRef.current);
}
seedPositions();
}, [mounted, seedPositions]);
window.addEventListener("resize", handleResize);
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 container = containerRef.current;
if (!container || isMobileRef.current) {
const rect = containerRectRef.current;
if (!rect || isMobileRef.current) {
return;
}
const rect = container.getBoundingClientRect();
mousePosRef.current = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
@@ -212,13 +247,8 @@ export default function AnimatedBackgroundContainer({
}, [handleMouseMove]);
useEffect(() => {
if (!mounted) {
return;
}
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
if (!canvas) {
return;
}
@@ -227,9 +257,25 @@ export default function AnimatedBackgroundContainer({
return;
}
const stopAnimation = () => {
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
};
const draw = () => {
const width = container.clientWidth;
const height = container.clientHeight;
if (!isVisibleRef.current || prefersReducedMotionRef.current) {
animationFrameRef.current = null;
return;
}
const { height, width } = canvasSizeRef.current;
if (width === 0 || height === 0) {
animationFrameRef.current = window.requestAnimationFrame(draw);
return;
}
frameRef.current += 1;
if (isMobileRef.current) {
@@ -241,14 +287,15 @@ export default function AnimatedBackgroundContainer({
const dx = mobileTargetRef.current.x - mobileAnchorRef.current.x;
const dy = mobileTargetRef.current.y - mobileAnchorRef.current.y;
if (Math.hypot(dx, dy) < 30) {
if (Math.hypot(dx, dy) < MOBILE_TARGET_DISTANCE) {
mobileTargetRef.current = {
x: rand(width * 0.15, width * 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 {
const desktopLerp = followSpeedRef.current;
smoothMouseRef.current.x +=
@@ -275,7 +322,13 @@ export default function AnimatedBackgroundContainer({
const edgeFade = Math.max(
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) {
@@ -293,15 +346,51 @@ export default function AnimatedBackgroundContainer({
animationFrameRef.current = window.requestAnimationFrame(draw);
};
animationFrameRef.current = window.requestAnimationFrame(draw);
return () => {
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
const startAnimation = () => {
if (
animationFrameRef.current === null &&
isVisibleRef.current &&
!prefersReducedMotionRef.current
) {
animationFrameRef.current = window.requestAnimationFrame(draw);
}
};
}, [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 (
<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 { 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 type { Entries } from "type-fest";
import { Button } from "~/components/ui/button";
@@ -16,6 +17,7 @@ const MultiBooleanFieldContext = createContext<MultiBooleanFieldContextProps|und
function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0: string[]) => void }) {
const context = useContext(MultiBooleanFieldContext)
const [searchBuffer, setSearchBuffer] = useState<string>("")
if (context === undefined) {
return (<></>)
}
@@ -43,19 +45,40 @@ function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0
}
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 (
<>
<ScrollArea onKeyDown={handleKeyDown} className="flex h-60">
{
params.options.map((opt) => (
<FormItem key={opt}>
<div className="flex flex-row justify-between py-2 border-b-1">
opt.startsWith(searchBuffer) && <FormItem key={opt}>
<div className="flex flex-row justify-between py-2 border-b">
<FormLabel>{opt}</FormLabel>
<Checkbox data-testid="multiboolean-checkbox" checked={checked(opt)} onCheckedChange={onCheckedItemChange(opt)} />
</div>
</FormItem>
))
}
</>
</ScrollArea>
)
}
export default function MultiBooleanFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, options: string[], defaultValues?: string[] }) {
@@ -76,9 +99,9 @@ export default function MultiBooleanFormField<T extends FieldValues>(params: { c
</PopoverTrigger>
<FormControl>
<PopoverContent data-testid="multiboolean-content">
<MultiBooleanFieldContext.Provider value={{checkedValues: checkedValues, setCheckedValue: setCheckedValues}}>
<InnerMultiBooleanFormField onChange={field.onChange} options={params.options} />
</MultiBooleanFieldContext.Provider>
<MultiBooleanFieldContext.Provider value={{checkedValues: checkedValues, setCheckedValue: setCheckedValues}}>
<InnerMultiBooleanFormField onChange={field.onChange} options={params.options} />
</MultiBooleanFieldContext.Provider>
</PopoverContent>
</FormControl>
</Popover>

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import type { IterableElement } from 'type-fest'
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
import { useEffect, useState } from "react";
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 { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
@@ -29,7 +29,8 @@ export default function CreateUpdateProjectForm(params: { className?: string, en
releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased",
releaseLink: params.entity ? params.entity.releaseLink : "",
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()
@@ -79,6 +80,7 @@ export default function CreateUpdateProjectForm(params: { className?: string, en
<SelectItem value="unreleased"> unreleased </SelectItem>
</SelectFormField>
<TextInputFormField control={form.control} label='Release Link' name='releaseLink' />
<IntInputFormField control={form.control} label='Order Position' name='orderPos'/>
</FormScaffold>
</FormMutationContextProvider>
)

View File

@@ -46,7 +46,7 @@ export default function CreateUpdateStackForm(params: { className?: string, enti
id={id}
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>
</FormMutationContextProvider>
</>

View File

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

View File

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

View File

@@ -30,9 +30,9 @@ export default function CvEntry(params: {
{
data.description ?
<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>
</div>
</article>
</CardContent> :
<></>
}

View File

@@ -125,6 +125,30 @@ const STACK_META: Record<string, { label: string; icon?: SvglIcon }> = {
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 }) {

View File

@@ -43,6 +43,7 @@ export const cvEntry = createTable(
.default(sql`CURRENT_TIMESTAMP`)
.notNull().$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 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(
"project",
@@ -68,6 +69,7 @@ export const project = createTable(
releaseStatus: releaseStatus(),
releaseLink: d.varchar({length: 200}),
stackId: d.uuid(),
orderPos: d.integer(),
})
)

View File

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

View File

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