From 962f1d798f2cb3c543503b4499c823a457240915 Mon Sep 17 00:00:00 2001 From: Thet Aung Date: Fri, 9 Jan 2026 11:59:37 +0300 Subject: [PATCH] improvements: examples and themes --- apps/client/src/App.tsx | 147 +++--- .../src/components/ui/dropdown-menu.tsx | 198 +++++++ apps/client/src/index.css | 455 +++++++++++++++-- apps/client/src/lib/trpc.ts | 4 +- apps/client/src/main.tsx | 10 +- apps/client/src/pages/AboutPage.tsx | 88 ---- apps/client/src/pages/HomePage.tsx | 58 --- apps/client/src/pages/NotFoundPage.tsx | 1 - apps/client/src/pages/TRPCDemoPage.tsx | 481 +++++++++++++++--- apps/server/router.ts | 75 +-- apps/server/routers/index.ts | 9 + apps/server/routers/posts.ts | 123 +++++ apps/server/routers/users.ts | 75 +++ 13 files changed, 1316 insertions(+), 408 deletions(-) create mode 100644 apps/client/src/components/ui/dropdown-menu.tsx delete mode 100644 apps/client/src/pages/AboutPage.tsx delete mode 100644 apps/client/src/pages/HomePage.tsx create mode 100644 apps/server/routers/index.ts create mode 100644 apps/server/routers/posts.ts create mode 100644 apps/server/routers/users.ts diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index e055d4c..516804c 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -1,94 +1,125 @@ -import { Moon, Sun } from "lucide-react"; +import { Moon, Sun, Database, Palette } from "lucide-react"; import { useEffect, useState } from "react"; -import { NavLink, Outlet } from "react-router-dom"; +import { Outlet } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; -const navItems = [ - { to: "/", label: "Home" }, - { to: "/about", label: "About" }, -]; +const themes = [ + { id: "purple", name: "Purple", color: "hsl(270 80% 60%)" }, + { id: "ocean", name: "Ocean", color: "hsl(210 90% 55%)" }, + { id: "sunset", name: "Sunset", color: "hsl(25 95% 55%)" }, + { id: "forest", name: "Forest", color: "hsl(145 70% 40%)" }, + { id: "rose", name: "Rose", color: "hsl(350 80% 60%)" }, + { id: "slate", name: "Slate", color: "hsl(220 15% 35%)" }, +] as const; -const getInitialTheme = (): "light" | "dark" => { - if (typeof window === "undefined") { - return "light"; - } - const stored = localStorage.getItem("theme"); - if (stored === "light" || stored === "dark") { - return stored; - } +type ThemeId = (typeof themes)[number]["id"]; +type Mode = "light" | "dark"; + +const getInitialTheme = (): ThemeId => { + if (typeof window === "undefined") return "purple"; + const stored = localStorage.getItem("color-theme"); + if (themes.some((t) => t.id === stored)) return stored as ThemeId; + return "purple"; +}; + +const getInitialMode = (): Mode => { + if (typeof window === "undefined") return "light"; + const stored = localStorage.getItem("mode"); + if (stored === "light" || stored === "dark") return stored; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; }; function App() { - const [theme, setTheme] = useState<"light" | "dark">(getInitialTheme); + const [theme, setTheme] = useState(getInitialTheme); + const [mode, setMode] = useState(getInitialMode); useEffect(() => { const root = document.documentElement; - root.classList.toggle("dark", theme === "dark"); - localStorage.setItem("theme", theme); - }, [theme]); + + // Remove existing theme classes + themes.forEach((t) => root.classList.remove(`theme-${t.id}`)); + + // Add current theme class + root.classList.add(`theme-${theme}`); + root.classList.toggle("dark", mode === "dark"); + + localStorage.setItem("color-theme", theme); + localStorage.setItem("mode", mode); + }, [theme, mode]); return (
-
+
- TaylorDB Starter - +
+ +
+ TaylorDB
+
+ {/* Theme Picker */} + + + + + + {themes.map((t) => ( + setTheme(t.id)} + className="flex items-center gap-3 cursor-pointer" + > +
+ + {t.name} + + {theme === t.id && ( + + )} + + ))} + + + + {/* Dark/Light Mode Toggle */} - -
-
+
diff --git a/apps/client/src/components/ui/dropdown-menu.tsx b/apps/client/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f5a07f9 --- /dev/null +++ b/apps/client/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/apps/client/src/index.css b/apps/client/src/index.css index 1f6add3..22bb1ea 100644 --- a/apps/client/src/index.css +++ b/apps/client/src/index.css @@ -29,49 +29,295 @@ } @layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + /* ======================================================================== + THEME: Purple (Default) - Vibrant purple with teal & pink accents + ======================================================================== */ + :root, + .theme-purple { + --background: 270 50% 98%; + --foreground: 270 50% 10%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 270 50% 10%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.75rem; + --popover-foreground: 270 50% 10%; + --primary: 270 80% 60%; + --primary-foreground: 0 0% 100%; + --secondary: 175 60% 45%; + --secondary-foreground: 0 0% 100%; + --muted: 270 30% 94%; + --muted-foreground: 270 20% 45%; + --accent: 330 80% 65%; + --accent-foreground: 0 0% 100%; + --destructive: 0 75% 55%; + --destructive-foreground: 0 0% 100%; + --border: 270 30% 88%; + --input: 270 30% 88%; + --ring: 270 80% 60%; + --radius: 1rem; + --theme-gradient: linear-gradient(135deg, hsl(270 80% 60%) 0%, hsl(330 85% 60%) 50%, hsl(175 70% 50%) 100%); } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; + .dark.theme-purple, + .dark:not([class*="theme-"]) { + --background: 270 40% 8%; + --foreground: 270 20% 95%; + --card: 270 40% 12%; + --card-foreground: 270 20% 95%; + --popover: 270 40% 12%; + --popover-foreground: 270 20% 95%; + --primary: 270 80% 65%; + --primary-foreground: 270 40% 8%; + --secondary: 175 70% 50%; + --secondary-foreground: 270 40% 8%; + --muted: 270 30% 18%; + --muted-foreground: 270 15% 65%; + --accent: 330 85% 60%; + --accent-foreground: 0 0% 100%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; + --border: 270 30% 22%; + --input: 270 30% 22%; + --ring: 270 80% 65%; + } + + /* ======================================================================== + THEME: Ocean - Deep blues with cyan accents + ======================================================================== */ + .theme-ocean { + --background: 210 50% 98%; + --foreground: 210 50% 10%; + --card: 0 0% 100%; + --card-foreground: 210 50% 10%; + --popover: 0 0% 100%; + --popover-foreground: 210 50% 10%; + --primary: 210 90% 55%; + --primary-foreground: 0 0% 100%; + --secondary: 185 80% 45%; + --secondary-foreground: 0 0% 100%; + --muted: 210 30% 94%; + --muted-foreground: 210 20% 45%; + --accent: 195 85% 50%; + --accent-foreground: 0 0% 100%; + --destructive: 0 75% 55%; + --destructive-foreground: 0 0% 100%; + --border: 210 30% 88%; + --input: 210 30% 88%; + --ring: 210 90% 55%; + --theme-gradient: linear-gradient(135deg, hsl(210 90% 55%) 0%, hsl(185 80% 45%) 50%, hsl(195 85% 50%) 100%); + } + + .dark.theme-ocean { + --background: 210 50% 6%; + --foreground: 210 20% 95%; + --card: 210 45% 10%; + --card-foreground: 210 20% 95%; + --popover: 210 45% 10%; + --popover-foreground: 210 20% 95%; + --primary: 210 85% 60%; + --primary-foreground: 210 50% 6%; + --secondary: 185 75% 50%; + --secondary-foreground: 210 50% 6%; + --muted: 210 35% 16%; + --muted-foreground: 210 15% 65%; + --accent: 195 80% 55%; + --accent-foreground: 0 0% 100%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; + --border: 210 35% 20%; + --input: 210 35% 20%; + --ring: 210 85% 60%; + } + + /* ======================================================================== + THEME: Sunset - Warm oranges and reds + ======================================================================== */ + .theme-sunset { + --background: 30 50% 98%; + --foreground: 20 50% 10%; + --card: 0 0% 100%; + --card-foreground: 20 50% 10%; + --popover: 0 0% 100%; + --popover-foreground: 20 50% 10%; + --primary: 25 95% 55%; + --primary-foreground: 0 0% 100%; + --secondary: 350 80% 55%; + --secondary-foreground: 0 0% 100%; + --muted: 30 30% 94%; + --muted-foreground: 20 20% 45%; + --accent: 45 90% 55%; + --accent-foreground: 20 50% 10%; + --destructive: 0 75% 55%; + --destructive-foreground: 0 0% 100%; + --border: 30 30% 88%; + --input: 30 30% 88%; + --ring: 25 95% 55%; + --theme-gradient: linear-gradient(135deg, hsl(25 95% 55%) 0%, hsl(350 80% 55%) 50%, hsl(45 90% 55%) 100%); + } + + .dark.theme-sunset { + --background: 20 40% 7%; + --foreground: 30 20% 95%; + --card: 20 40% 11%; + --card-foreground: 30 20% 95%; + --popover: 20 40% 11%; + --popover-foreground: 30 20% 95%; + --primary: 25 90% 58%; + --primary-foreground: 20 40% 7%; + --secondary: 350 75% 58%; + --secondary-foreground: 0 0% 100%; + --muted: 20 30% 16%; + --muted-foreground: 30 15% 65%; + --accent: 45 85% 58%; + --accent-foreground: 20 50% 10%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; + --border: 20 30% 20%; + --input: 20 30% 20%; + --ring: 25 90% 58%; + } + + /* ======================================================================== + THEME: Forest - Earthy greens with warm accents + ======================================================================== */ + .theme-forest { + --background: 140 30% 97%; + --foreground: 140 40% 10%; + --card: 0 0% 100%; + --card-foreground: 140 40% 10%; + --popover: 0 0% 100%; + --popover-foreground: 140 40% 10%; + --primary: 145 70% 40%; + --primary-foreground: 0 0% 100%; + --secondary: 85 55% 45%; + --secondary-foreground: 0 0% 100%; + --muted: 140 20% 93%; + --muted-foreground: 140 15% 40%; + --accent: 35 80% 50%; + --accent-foreground: 0 0% 100%; + --destructive: 0 75% 55%; + --destructive-foreground: 0 0% 100%; + --border: 140 20% 87%; + --input: 140 20% 87%; + --ring: 145 70% 40%; + --theme-gradient: linear-gradient(135deg, hsl(145 70% 40%) 0%, hsl(85 55% 45%) 50%, hsl(35 80% 50%) 100%); + } + + .dark.theme-forest { + --background: 140 35% 7%; + --foreground: 140 15% 95%; + --card: 140 35% 11%; + --card-foreground: 140 15% 95%; + --popover: 140 35% 11%; + --popover-foreground: 140 15% 95%; + --primary: 145 65% 45%; + --primary-foreground: 140 35% 7%; + --secondary: 85 50% 50%; + --secondary-foreground: 140 35% 7%; + --muted: 140 25% 16%; + --muted-foreground: 140 10% 65%; + --accent: 35 75% 55%; + --accent-foreground: 0 0% 100%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; + --border: 140 25% 20%; + --input: 140 25% 20%; + --ring: 145 65% 45%; + } + + /* ======================================================================== + THEME: Rose - Soft pinks with elegant accents + ======================================================================== */ + .theme-rose { + --background: 350 50% 98%; + --foreground: 350 40% 10%; + --card: 0 0% 100%; + --card-foreground: 350 40% 10%; + --popover: 0 0% 100%; + --popover-foreground: 350 40% 10%; + --primary: 350 80% 60%; + --primary-foreground: 0 0% 100%; + --secondary: 320 70% 55%; + --secondary-foreground: 0 0% 100%; + --muted: 350 25% 94%; + --muted-foreground: 350 20% 45%; + --accent: 15 85% 60%; + --accent-foreground: 0 0% 100%; + --destructive: 0 75% 55%; + --destructive-foreground: 0 0% 100%; + --border: 350 25% 88%; + --input: 350 25% 88%; + --ring: 350 80% 60%; + --theme-gradient: linear-gradient(135deg, hsl(350 80% 60%) 0%, hsl(320 70% 55%) 50%, hsl(15 85% 60%) 100%); + } + + .dark.theme-rose { + --background: 350 35% 7%; + --foreground: 350 15% 95%; + --card: 350 35% 11%; + --card-foreground: 350 15% 95%; + --popover: 350 35% 11%; + --popover-foreground: 350 15% 95%; + --primary: 350 75% 62%; + --primary-foreground: 350 35% 7%; + --secondary: 320 65% 58%; + --secondary-foreground: 0 0% 100%; + --muted: 350 25% 16%; + --muted-foreground: 350 12% 65%; + --accent: 15 80% 62%; + --accent-foreground: 0 0% 100%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; + --border: 350 25% 20%; + --input: 350 25% 20%; + --ring: 350 75% 62%; + } + + /* ======================================================================== + THEME: Slate - Minimal and professional + ======================================================================== */ + .theme-slate { + --background: 0 0% 98%; + --foreground: 220 15% 15%; + --card: 0 0% 100%; + --card-foreground: 220 15% 15%; + --popover: 0 0% 100%; + --popover-foreground: 220 15% 15%; + --primary: 220 15% 25%; + --primary-foreground: 0 0% 100%; + --secondary: 220 10% 50%; + --secondary-foreground: 0 0% 100%; + --muted: 220 10% 94%; + --muted-foreground: 220 10% 45%; + --accent: 220 15% 35%; + --accent-foreground: 0 0% 100%; + --destructive: 0 75% 55%; + --destructive-foreground: 0 0% 100%; + --border: 220 10% 88%; + --input: 220 10% 88%; + --ring: 220 15% 25%; + --theme-gradient: linear-gradient(135deg, hsl(220 15% 25%) 0%, hsl(220 10% 50%) 50%, hsl(220 15% 35%) 100%); + } + + .dark.theme-slate { + --background: 220 20% 6%; + --foreground: 220 10% 95%; + --card: 220 18% 10%; + --card-foreground: 220 10% 95%; + --popover: 220 18% 10%; + --popover-foreground: 220 10% 95%; + --primary: 220 12% 70%; + --primary-foreground: 220 20% 6%; + --secondary: 220 10% 55%; + --secondary-foreground: 0 0% 100%; + --muted: 220 15% 15%; + --muted-foreground: 220 8% 60%; + --accent: 220 12% 45%; + --accent-foreground: 0 0% 100%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; + --border: 220 15% 18%; + --input: 220 15% 18%; + --ring: 220 12% 70%; } * { @@ -93,20 +339,127 @@ } } +/* Animated gradient background - uses theme gradient */ +.gradient-bg { + background: linear-gradient( + 135deg, + hsl(var(--primary) / 0.08) 0%, + hsl(var(--secondary) / 0.08) 50%, + hsl(var(--accent) / 0.08) 100% + ); + animation: gradient-shift 8s ease infinite; + background-size: 200% 200%; +} + +@keyframes gradient-shift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +/* Card hover effects */ +.card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.card-hover:hover { + box-shadow: 0 20px 40px -12px hsl(var(--primary) / 0.25); +} + +/* Glassmorphism card */ +.glass-card { + background: hsl(var(--card) / 0.8); + backdrop-filter: blur(12px); + border: 1px solid hsl(var(--border) / 0.5); +} + +/* Gradient text - uses theme colors */ +.gradient-text { + background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 50%, hsl(var(--secondary)) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Glow effects - use theme primary */ +.glow-primary { + box-shadow: 0 0 20px hsl(var(--primary) / 0.3), 0 0 40px hsl(var(--primary) / 0.1); +} + +.glow-secondary { + box-shadow: 0 0 20px hsl(var(--secondary) / 0.3), 0 0 40px hsl(var(--secondary) / 0.1); +} + +.glow-accent { + box-shadow: 0 0 20px hsl(var(--accent) / 0.3), 0 0 40px hsl(var(--accent) / 0.1); +} + +/* Item row hover */ +.item-row { + transition: all 0.2s ease; +} + +.item-row:hover { + background: hsl(var(--muted) / 0.8); +} + +/* Status badges */ +.badge-success { + background: linear-gradient(135deg, hsl(160 80% 40% / 0.2) 0%, hsl(175 70% 50% / 0.2) 100%); + color: hsl(160 80% 40%); + border: 1px solid hsl(160 80% 40% / 0.3); +} + +.dark .badge-success { + background: linear-gradient(135deg, hsl(160 70% 45% / 0.2) 0%, hsl(175 70% 50% / 0.2) 100%); + color: hsl(160 70% 65%); +} + +.badge-warning { + background: linear-gradient(135deg, hsl(45 90% 50% / 0.2) 0%, hsl(35 90% 55% / 0.2) 100%); + color: hsl(35 90% 40%); + border: 1px solid hsl(45 90% 50% / 0.3); +} + +.dark .badge-warning { + background: linear-gradient(135deg, hsl(45 90% 50% / 0.2) 0%, hsl(35 90% 55% / 0.2) 100%); + color: hsl(45 90% 65%); +} + +/* Pulse animation for loader */ +.pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 1; filter: drop-shadow(0 0 8px hsl(var(--primary) / 0.5)); } + 50% { opacity: 0.7; filter: drop-shadow(0 0 16px hsl(var(--primary) / 0.8)); } +} + +/* Theme picker swatch */ +.theme-swatch { + width: 1.25rem; + height: 1.25rem; + border-radius: 9999px; + border: 2px solid transparent; + transition: all 0.2s ease; + cursor: pointer; +} + +.theme-swatch:hover { + transform: scale(1.1); +} + +.theme-swatch.active { + border-color: hsl(var(--foreground)); + box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--foreground) / 0.3); +} + @keyframes accordion-down { - from { - height: 0; - } - to { - height: var(--radix-accordion-content-height); - } + from { height: 0; } + to { height: var(--radix-accordion-content-height); } } @keyframes accordion-up { - from { - height: var(--radix-accordion-content-height); - } - to { - height: 0; - } + from { height: var(--radix-accordion-content-height); } + to { height: 0; } } \ No newline at end of file diff --git a/apps/client/src/lib/trpc.ts b/apps/client/src/lib/trpc.ts index 9db6c1d..0b8e077 100644 --- a/apps/client/src/lib/trpc.ts +++ b/apps/client/src/lib/trpc.ts @@ -14,10 +14,10 @@ export const trpc: CreateTRPCReact = function getBaseUrl() { if (typeof window !== "undefined") { // Browser: use relative URL or environment variable - return import.meta.env.VITE_TRPC_URL || "http://localhost:3001"; + return import.meta.env.VITE_TRPC_URL || "http://localhost:3001/api"; } // SSR: assume localhost - return "http://localhost:3001"; + return "http://localhost:3001/api"; } /** diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index 57167b0..642f0b7 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -5,10 +5,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./App"; import "./index.css"; -import AboutPage from "./pages/AboutPage"; -import HomePage from "./pages/HomePage"; -import NotFoundPage from "./pages/NotFoundPage"; import TRPCDemoPage from "./pages/TRPCDemoPage"; +import NotFoundPage from "./pages/NotFoundPage"; import { trpc, trpcClient } from "./lib/trpc"; // Create a React Query client @@ -24,11 +22,7 @@ const router = createBrowserRouter([ { path: "/", element: , - children: [ - { index: true, element: }, - { path: "about", element: }, - { path: "trpc-demo", element: }, - ], + children: [{ index: true, element: }], }, { path: "*", element: }, ]); diff --git a/apps/client/src/pages/AboutPage.tsx b/apps/client/src/pages/AboutPage.tsx deleted file mode 100644 index 8beda39..0000000 --- a/apps/client/src/pages/AboutPage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { ExternalLink } from "lucide-react"; -import { Link } from "react-router-dom"; - -import { Button } from "@/components/ui/button"; - -const AboutPage = () => { - return ( -
-
-

About This Template

-

- This is a full-stack template for building modern web applications - with TaylorDB. It includes React + Vite frontend, Node.js + tRPC - backend, and shadcn/ui components. -

-
- -
-
-

Type-Safe APIs

-

- Full end-to-end type safety from database to UI using tRPC and - TypeScript. Auto-generated types from your TaylorDB schema. -

- -
- -
-

Modern UI

-

- Built with shadcn/ui and Tailwind CSS. Responsive, accessible, and - customizable components with dark mode support. -

- -
-
- -
-

Documentation

-
    -
  • - 📘 AGENTS.md - AI agent - instructions and development workflow -
  • -
  • - 📗{" "} - docs/TAYLORDB_QUERY_REFERENCE.md{" "} - - Complete query builder examples -
  • -
  • - 📙{" "} - docs/SHADCN_COMPONENTS_GUIDE.md - - UI component patterns for dashboards -
  • -
-
- - -
- ); -}; - -export default AboutPage; diff --git a/apps/client/src/pages/HomePage.tsx b/apps/client/src/pages/HomePage.tsx deleted file mode 100644 index 5c0e5c1..0000000 --- a/apps/client/src/pages/HomePage.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ArrowRight } from "lucide-react"; -import { Link } from "react-router-dom"; - -import { Button } from "@/components/ui/button"; - -const HomePage = () => { - return ( -
-
-

- Welcome -

-

- Build your TaylorDB UI with React Router + shadcn/ui -

-

- Routing, Tailwind, and shadcn/ui are wired up. Start connecting - components to your TaylorDB data using the generated client and types. -

-
- -
-

- Animation check: this card fades in using{" "} - animate-in fade-in from tw-animate-css. -

-

- If you see a smooth fade on load, the plugin is wired correctly. -

-
- -
- - - -
-
- ); -}; - -export default HomePage; diff --git a/apps/client/src/pages/NotFoundPage.tsx b/apps/client/src/pages/NotFoundPage.tsx index 4f3fba2..d19adf3 100644 --- a/apps/client/src/pages/NotFoundPage.tsx +++ b/apps/client/src/pages/NotFoundPage.tsx @@ -24,4 +24,3 @@ const NotFoundPage = () => { }; export default NotFoundPage; - diff --git a/apps/client/src/pages/TRPCDemoPage.tsx b/apps/client/src/pages/TRPCDemoPage.tsx index 2fd028d..77d3cbf 100644 --- a/apps/client/src/pages/TRPCDemoPage.tsx +++ b/apps/client/src/pages/TRPCDemoPage.tsx @@ -10,88 +10,108 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { trpc } from "@/lib/trpc"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon, Loader2 } from "lucide-react"; +import { + Loader2, + Plus, + Trash2, + Edit2, + Check, + X, + Sparkles, + Users, + FileText, + Zap, +} from "lucide-react"; export default function TRPCDemoPage() { return ( -
-
-

tRPC + TaylorDB Demo

-

- Example of type-safe API calls with tRPC -

-
+
+
+ {/* Hero Header */} +
+
+ + Type-safe API Demo +
+

+ tRPC + TaylorDB +

+

+ Experience the power of end-to-end type safety with a beautiful, + modern interface +

+
-
- {/* Info Alert */} - - - Template Example - - This page demonstrates a simple tRPC query. Replace this with your - own queries based on your TaylorDB schema. See{" "} - apps/server/router.ts to - add more procedures. - - +
+ + + +
- {/* Example: Hello Query */} - + {/* Footer */} +
+

Built with 💜 using tRPC, React Query & TaylorDB

+
); } // ============================================================================ -// Example Component: Simple Query +// Hello Query Example // ============================================================================ function HelloExample() { const [name, setName] = useState(""); const { data, isLoading, refetch } = trpc.hello.useQuery( { name: name || undefined }, - { enabled: false } // Only run when user clicks button + { enabled: false } ); - const handleQuery = () => { - refetch(); - }; - return ( - - - Example: Hello Query - - A simple tRPC query to test the connection - + + +
+
+ +
+
+ Hello Query + + Simple query to test the connection + +
+
-
- +
setName(e.target.value)} placeholder="Enter your name..." + className="flex-1" /> +
- - - {data && ( -
-

Response:

-
{JSON.stringify(data, null, 2)}
+
+
+
+              {JSON.stringify(data, null, 2)}
+            
)} @@ -99,30 +119,331 @@ function HelloExample() { ); } -/** - * ============================================================================ - * Add Your Own Components Here - * ============================================================================ - * - * Follow this pattern to create your own tRPC queries and mutations: - * - * 1. Create procedures in apps/server/router.ts - * 2. Use trpc..useQuery() for queries (reading data) - * 3. Use trpc..useMutation() for mutations (writing data) - * 4. Handle loading and error states - * 5. Refetch queries after mutations to update the UI - * - * Example Query: - * const { data, isLoading, error } = trpc.items.getAll.useQuery(); - * - * Example Mutation: - * const createMutation = trpc.items.create.useMutation({ - * onSuccess: () => { - * // Refetch or invalidate queries - * }, - * }); - * - * For comprehensive examples, see: - * - docs/SHADCN_COMPONENTS_GUIDE.md - UI component patterns - * - AGENTS.md - Complete development workflow - */ +// ============================================================================ +// Users CRUD Example +// ============================================================================ + +function UsersExample() { + const [newName, setNewName] = useState(""); + const [newEmail, setNewEmail] = useState(""); + const [editingId, setEditingId] = useState(null); + const [editName, setEditName] = useState(""); + const [editEmail, setEditEmail] = useState(""); + + const utils = trpc.useUtils(); + const { data: users, isLoading } = trpc.users.getAll.useQuery(); + + const createMutation = trpc.users.create.useMutation({ + onSuccess: () => { + utils.users.getAll.invalidate(); + setNewName(""); + setNewEmail(""); + }, + }); + + const updateMutation = trpc.users.update.useMutation({ + onSuccess: () => { + utils.users.getAll.invalidate(); + setEditingId(null); + }, + }); + + const deleteMutation = trpc.users.delete.useMutation({ + onSuccess: () => utils.users.getAll.invalidate(), + }); + + const startEditing = (user: { id: number; name: string; email: string }) => { + setEditingId(user.id); + setEditName(user.name); + setEditEmail(user.email); + }; + + return ( + + +
+
+ +
+
+ Users + Full CRUD operations example +
+
+
+ + {/* Create Form */} +
+ setNewName(e.target.value)} + placeholder="Name" + className="flex-1" + /> + setNewEmail(e.target.value)} + placeholder="Email" + className="flex-1" + /> + +
+ + {/* Users List */} + {isLoading ? ( +
+ +
+ ) : ( +
+ {users?.length === 0 && ( +

+ No users yet. Create one above! +

+ )} + {users?.map((user) => ( +
+ {editingId === user.id ? ( + <> + setEditName(e.target.value)} + className="flex-1" + /> + setEditEmail(e.target.value)} + className="flex-1" + /> + + + + ) : ( + <> +
+ {user.name.charAt(0).toUpperCase()} +
+
+
{user.name}
+
+ {user.email} +
+
+ + + + )} +
+ ))} +
+ )} +
+
+ ); +} + +// ============================================================================ +// Posts Example +// ============================================================================ + +function PostsExample() { + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [authorId, setAuthorId] = useState("1"); + + const utils = trpc.useUtils(); + const { data: posts, isLoading } = trpc.posts.getAll.useQuery(); + + const createMutation = trpc.posts.create.useMutation({ + onSuccess: () => { + utils.posts.getAll.invalidate(); + setTitle(""); + setContent(""); + }, + }); + + const publishMutation = trpc.posts.publish.useMutation({ + onSuccess: () => utils.posts.getAll.invalidate(), + }); + + const deleteMutation = trpc.posts.delete.useMutation({ + onSuccess: () => utils.posts.getAll.invalidate(), + }); + + return ( + + +
+
+ +
+
+ Posts + With publish action and filtering +
+
+
+ + {/* Create Form */} +
+
+ setTitle(e.target.value)} + placeholder="Post title..." + className="flex-1" + /> +
+ + setAuthorId(e.target.value)} + type="number" + /> +
+
+
+ setContent(e.target.value)} + placeholder="Write something amazing..." + className="flex-1" + /> + +
+
+ + {/* Posts List */} + {isLoading ? ( +
+ +
+ ) : ( +
+ {posts?.length === 0 && ( +

+ No posts yet. Create your first post! +

+ )} + {posts?.map((post) => ( +
+
+
+
+

{post.title}

+ + {post.published ? "Published" : "Draft"} + +
+

+ {post.content} +

+
+
+ {!post.published && ( + + )} + +
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/apps/server/router.ts b/apps/server/router.ts index f84b759..beba861 100644 --- a/apps/server/router.ts +++ b/apps/server/router.ts @@ -1,34 +1,29 @@ import { z } from "zod"; import { router, publicProcedure } from "./trpc"; -// import * as db from "./taylordb/query-builder"; +import { usersRouter, postsRouter } from "./routers"; /** * Main tRPC Router * - * This is your main API router. Define all your procedures here. - * Group related procedures together for better organization. + * This router merges all sub-routers from the routers/ directory. + * Each domain (users, posts, etc.) has its own file for better organization. * - * Example structure: - * - * export const appRouter = router({ - * users: { - * getAll: publicProcedure.query(async () => { ... }), - * getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { ... }), - * create: publicProcedure.input(z.object({ ... })).mutation(async ({ input }) => { ... }), - * update: publicProcedure.input(z.object({ ... })).mutation(async ({ input }) => { ... }), - * delete: publicProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { ... }), - * }, - * posts: { - * // ... - * }, - * }); + * To add a new domain: + * 1. Create a new file in routers/ (e.g., routers/comments.ts) + * 2. Export the router from routers/index.ts + * 3. Import and add it below */ export const appRouter = router({ // ============================================================================ - // Example / Test Procedures + // Sub-Routers (organized by domain) // ============================================================================ + users: usersRouter, + posts: postsRouter, + // ============================================================================ + // Global / Utility Procedures + // ============================================================================ hello: publicProcedure .input( z @@ -44,50 +39,6 @@ export const appRouter = router({ uptime: process.uptime(), }; }), - - // ============================================================================ - // Your API Procedures - // ============================================================================ - // - // Add your procedures here following this pattern: - // - // tableName: { - // getAll: publicProcedure.query(async () => { - // return await db.getAllRecords(); - // }), - // - // getById: publicProcedure - // .input(z.object({ id: z.number() })) - // .query(async ({ input }) => { - // return await db.getRecordById(input.id); - // }), - // - // create: publicProcedure - // .input(z.object({ - // name: z.string().min(1), - // status: z.string() - // })) - // .mutation(async ({ input }) => { - // return await db.createRecord(input); - // }), - // - // update: publicProcedure - // .input(z.object({ - // id: z.number(), - // name: z.string().optional(), - // status: z.string().optional() - // })) - // .mutation(async ({ input }) => { - // const { id, ...data } = input; - // return await db.updateRecord(id, data); - // }), - // - // delete: publicProcedure - // .input(z.object({ id: z.number() })) - // .mutation(async ({ input }) => { - // return await db.deleteRecord(input.id); - // }), - // }, }); // Export type definition of API diff --git a/apps/server/routers/index.ts b/apps/server/routers/index.ts new file mode 100644 index 0000000..174edbe --- /dev/null +++ b/apps/server/routers/index.ts @@ -0,0 +1,9 @@ +/** + * Routers Index + * + * Re-export all sub-routers from a single entry point. + * This keeps imports clean in the main router.ts + */ + +export { usersRouter } from "./users"; +export { postsRouter } from "./posts"; diff --git a/apps/server/routers/posts.ts b/apps/server/routers/posts.ts new file mode 100644 index 0000000..b12106f --- /dev/null +++ b/apps/server/routers/posts.ts @@ -0,0 +1,123 @@ +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +/** + * Posts Router + * + * Another example sub-router showing a different domain. + * Demonstrates relationships (author references users). + */ + +// In-memory store for demonstration +const posts: { + id: number; + title: string; + content: string; + authorId: number; + published: boolean; + createdAt: Date; +}[] = [ + { + id: 1, + title: "Hello World", + content: "This is my first post!", + authorId: 1, + published: true, + createdAt: new Date(), + }, +]; +let nextId = 2; + +export const postsRouter = router({ + getAll: publicProcedure + .input( + z + .object({ + published: z.boolean().optional(), + authorId: z.number().optional(), + }) + .optional() + ) + .query(({ input }) => { + let result = posts; + + if (input?.published !== undefined) { + result = result.filter((p) => p.published === input.published); + } + if (input?.authorId !== undefined) { + result = result.filter((p) => p.authorId === input.authorId); + } + + return result; + }), + + getById: publicProcedure + .input(z.object({ id: z.number() })) + .query(({ input }) => { + const post = posts.find((p) => p.id === input.id); + if (!post) throw new Error("Post not found"); + return post; + }), + + create: publicProcedure + .input( + z.object({ + title: z.string().min(1).max(200), + content: z.string().min(1), + authorId: z.number(), + published: z.boolean().default(false), + }) + ) + .mutation(({ input }) => { + const newPost = { + id: nextId++, + title: input.title, + content: input.content, + authorId: input.authorId, + published: input.published, + createdAt: new Date(), + }; + posts.push(newPost); + return newPost; + }), + + update: publicProcedure + .input( + z.object({ + id: z.number(), + title: z.string().min(1).max(200).optional(), + content: z.string().min(1).optional(), + published: z.boolean().optional(), + }) + ) + .mutation(({ input }) => { + const post = posts.find((p) => p.id === input.id); + if (!post) throw new Error("Post not found"); + + if (input.title) post.title = input.title; + if (input.content) post.content = input.content; + if (input.published !== undefined) post.published = input.published; + return post; + }), + + delete: publicProcedure + .input(z.object({ id: z.number() })) + .mutation(({ input }) => { + const index = posts.findIndex((p) => p.id === input.id); + if (index === -1) throw new Error("Post not found"); + + const [deleted] = posts.splice(index, 1); + return deleted; + }), + + // Example of a more specific procedure + publish: publicProcedure + .input(z.object({ id: z.number() })) + .mutation(({ input }) => { + const post = posts.find((p) => p.id === input.id); + if (!post) throw new Error("Post not found"); + + post.published = true; + return post; + }), +}); diff --git a/apps/server/routers/users.ts b/apps/server/routers/users.ts new file mode 100644 index 0000000..f0e157e --- /dev/null +++ b/apps/server/routers/users.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +/** + * Users Router + * + * Example sub-router demonstrating CRUD operations. + * Replace with your actual taylordb implementation. + */ + +// In-memory store for demonstration +const users: { id: number; name: string; email: string; createdAt: Date }[] = [ + { id: 1, name: "Alice", email: "alice@example.com", createdAt: new Date() }, + { id: 2, name: "Bob", email: "bob@example.com", createdAt: new Date() }, +]; +let nextId = 3; + +export const usersRouter = router({ + getAll: publicProcedure.query(() => { + return users; + }), + + getById: publicProcedure + .input(z.object({ id: z.number() })) + .query(({ input }) => { + const user = users.find((u) => u.id === input.id); + if (!user) throw new Error("User not found"); + return user; + }), + + create: publicProcedure + .input( + z.object({ + name: z.string().min(1), + email: z.string().email(), + }) + ) + .mutation(({ input }) => { + const newUser = { + id: nextId++, + name: input.name, + email: input.email, + createdAt: new Date(), + }; + users.push(newUser); + return newUser; + }), + + update: publicProcedure + .input( + z.object({ + id: z.number(), + name: z.string().min(1).optional(), + email: z.string().email().optional(), + }) + ) + .mutation(({ input }) => { + const user = users.find((u) => u.id === input.id); + if (!user) throw new Error("User not found"); + + if (input.name) user.name = input.name; + if (input.email) user.email = input.email; + return user; + }), + + delete: publicProcedure + .input(z.object({ id: z.number() })) + .mutation(({ input }) => { + const index = users.findIndex((u) => u.id === input.id); + if (index === -1) throw new Error("User not found"); + + const [deleted] = users.splice(index, 1); + return deleted; + }), +});