From 38e107ad59330ba415cce4730a3fdf871290a250 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 6 Sep 2025 18:48:53 +0200 Subject: [PATCH] feat: add UI components including Skeleton, Slider, Toaster, Switch, Table, Tabs, Textarea, Toggle Group, Toggle, Tooltip, and User Avatar - Implemented Skeleton component for loading states. - Added Slider component using Radix UI for customizable sliders. - Created Toaster component for notifications with theme support. - Developed Switch component for toggle functionality. - Introduced Table component with subcomponents for structured data display. - Built Tabs component for tabbed navigation. - Added Textarea component for multi-line text input. - Implemented Toggle Group and Toggle components for grouped toggle buttons. - Created Tooltip component for displaying additional information on hover. - Added User Avatar component for displaying user images with fallback. - Implemented useIsMobile hook for responsive design. - Created API utility functions for user and thought data fetching. - Added utility function for class name merging. - Updated package.json with new dependencies for UI components and utilities. - Added TypeScript configuration for path aliasing. --- thoughts-backend/models/src/schemas/user.rs | 6 + thoughts-frontend/app/globals.css | 124 ++- thoughts-frontend/app/page.tsx | 56 +- .../app/users/[username]/loading.tsx | 30 + .../app/users/[username]/page.tsx | 86 +++ thoughts-frontend/bun.lock | 292 ++++++- thoughts-frontend/components.json | 22 + thoughts-frontend/components/thought-card.tsx | 33 + thoughts-frontend/components/ui/accordion.tsx | 66 ++ .../components/ui/alert-dialog.tsx | 157 ++++ thoughts-frontend/components/ui/alert.tsx | 66 ++ .../components/ui/aspect-ratio.tsx | 11 + thoughts-frontend/components/ui/avatar.tsx | 53 ++ thoughts-frontend/components/ui/badge.tsx | 46 ++ .../components/ui/breadcrumb.tsx | 109 +++ thoughts-frontend/components/ui/button.tsx | 59 ++ thoughts-frontend/components/ui/calendar.tsx | 213 +++++ thoughts-frontend/components/ui/card.tsx | 92 +++ thoughts-frontend/components/ui/carousel.tsx | 241 ++++++ thoughts-frontend/components/ui/chart.tsx | 353 +++++++++ thoughts-frontend/components/ui/checkbox.tsx | 32 + .../components/ui/collapsible.tsx | 33 + thoughts-frontend/components/ui/command.tsx | 184 +++++ .../components/ui/context-menu.tsx | 252 ++++++ thoughts-frontend/components/ui/dialog.tsx | 143 ++++ thoughts-frontend/components/ui/drawer.tsx | 135 ++++ .../components/ui/dropdown-menu.tsx | 257 +++++++ thoughts-frontend/components/ui/form.tsx | 167 ++++ .../components/ui/hover-card.tsx | 44 ++ thoughts-frontend/components/ui/input-otp.tsx | 77 ++ thoughts-frontend/components/ui/input.tsx | 21 + thoughts-frontend/components/ui/label.tsx | 24 + thoughts-frontend/components/ui/menubar.tsx | 276 +++++++ .../components/ui/navigation-menu.tsx | 168 ++++ .../components/ui/pagination.tsx | 127 +++ thoughts-frontend/components/ui/popover.tsx | 48 ++ thoughts-frontend/components/ui/progress.tsx | 31 + .../components/ui/radio-group.tsx | 45 ++ thoughts-frontend/components/ui/resizable.tsx | 56 ++ .../components/ui/scroll-area.tsx | 58 ++ thoughts-frontend/components/ui/select.tsx | 185 +++++ thoughts-frontend/components/ui/separator.tsx | 28 + thoughts-frontend/components/ui/sheet.tsx | 139 ++++ thoughts-frontend/components/ui/sidebar.tsx | 726 ++++++++++++++++++ thoughts-frontend/components/ui/skeleton.tsx | 13 + thoughts-frontend/components/ui/slider.tsx | 63 ++ thoughts-frontend/components/ui/sonner.tsx | 25 + thoughts-frontend/components/ui/switch.tsx | 31 + thoughts-frontend/components/ui/table.tsx | 116 +++ thoughts-frontend/components/ui/tabs.tsx | 66 ++ thoughts-frontend/components/ui/textarea.tsx | 18 + .../components/ui/toggle-group.tsx | 73 ++ thoughts-frontend/components/ui/toggle.tsx | 47 ++ thoughts-frontend/components/ui/tooltip.tsx | 61 ++ thoughts-frontend/components/user-avatar.tsx | 18 + thoughts-frontend/hooks/use-mobile.ts | 19 + thoughts-frontend/lib/api.ts | 59 ++ thoughts-frontend/lib/utils.ts | 6 + thoughts-frontend/package.json | 54 +- thoughts-frontend/tsconfig.app.json | 9 + 60 files changed, 6000 insertions(+), 49 deletions(-) create mode 100644 thoughts-frontend/app/users/[username]/loading.tsx create mode 100644 thoughts-frontend/app/users/[username]/page.tsx create mode 100644 thoughts-frontend/components.json create mode 100644 thoughts-frontend/components/thought-card.tsx create mode 100644 thoughts-frontend/components/ui/accordion.tsx create mode 100644 thoughts-frontend/components/ui/alert-dialog.tsx create mode 100644 thoughts-frontend/components/ui/alert.tsx create mode 100644 thoughts-frontend/components/ui/aspect-ratio.tsx create mode 100644 thoughts-frontend/components/ui/avatar.tsx create mode 100644 thoughts-frontend/components/ui/badge.tsx create mode 100644 thoughts-frontend/components/ui/breadcrumb.tsx create mode 100644 thoughts-frontend/components/ui/button.tsx create mode 100644 thoughts-frontend/components/ui/calendar.tsx create mode 100644 thoughts-frontend/components/ui/card.tsx create mode 100644 thoughts-frontend/components/ui/carousel.tsx create mode 100644 thoughts-frontend/components/ui/chart.tsx create mode 100644 thoughts-frontend/components/ui/checkbox.tsx create mode 100644 thoughts-frontend/components/ui/collapsible.tsx create mode 100644 thoughts-frontend/components/ui/command.tsx create mode 100644 thoughts-frontend/components/ui/context-menu.tsx create mode 100644 thoughts-frontend/components/ui/dialog.tsx create mode 100644 thoughts-frontend/components/ui/drawer.tsx create mode 100644 thoughts-frontend/components/ui/dropdown-menu.tsx create mode 100644 thoughts-frontend/components/ui/form.tsx create mode 100644 thoughts-frontend/components/ui/hover-card.tsx create mode 100644 thoughts-frontend/components/ui/input-otp.tsx create mode 100644 thoughts-frontend/components/ui/input.tsx create mode 100644 thoughts-frontend/components/ui/label.tsx create mode 100644 thoughts-frontend/components/ui/menubar.tsx create mode 100644 thoughts-frontend/components/ui/navigation-menu.tsx create mode 100644 thoughts-frontend/components/ui/pagination.tsx create mode 100644 thoughts-frontend/components/ui/popover.tsx create mode 100644 thoughts-frontend/components/ui/progress.tsx create mode 100644 thoughts-frontend/components/ui/radio-group.tsx create mode 100644 thoughts-frontend/components/ui/resizable.tsx create mode 100644 thoughts-frontend/components/ui/scroll-area.tsx create mode 100644 thoughts-frontend/components/ui/select.tsx create mode 100644 thoughts-frontend/components/ui/separator.tsx create mode 100644 thoughts-frontend/components/ui/sheet.tsx create mode 100644 thoughts-frontend/components/ui/sidebar.tsx create mode 100644 thoughts-frontend/components/ui/skeleton.tsx create mode 100644 thoughts-frontend/components/ui/slider.tsx create mode 100644 thoughts-frontend/components/ui/sonner.tsx create mode 100644 thoughts-frontend/components/ui/switch.tsx create mode 100644 thoughts-frontend/components/ui/table.tsx create mode 100644 thoughts-frontend/components/ui/tabs.tsx create mode 100644 thoughts-frontend/components/ui/textarea.tsx create mode 100644 thoughts-frontend/components/ui/toggle-group.tsx create mode 100644 thoughts-frontend/components/ui/toggle.tsx create mode 100644 thoughts-frontend/components/ui/tooltip.tsx create mode 100644 thoughts-frontend/components/user-avatar.tsx create mode 100644 thoughts-frontend/hooks/use-mobile.ts create mode 100644 thoughts-frontend/lib/api.ts create mode 100644 thoughts-frontend/lib/utils.ts create mode 100644 thoughts-frontend/tsconfig.app.json diff --git a/thoughts-backend/models/src/schemas/user.rs b/thoughts-backend/models/src/schemas/user.rs index fc862a5..44013a0 100644 --- a/thoughts-backend/models/src/schemas/user.rs +++ b/thoughts-backend/models/src/schemas/user.rs @@ -9,12 +9,18 @@ use crate::domains::user; pub struct UserSchema { pub id: Uuid, pub username: String, + #[serde(rename = "displayName")] pub display_name: Option, pub bio: Option, + #[serde(rename = "avatarUrl")] pub avatar_url: Option, + #[serde(rename = "headerUrl")] pub header_url: Option, + #[serde(rename = "customCss")] pub custom_css: Option, + #[serde(rename = "topFriends")] pub top_friends: Vec, + #[serde(rename = "joinedAt")] pub joined_at: DateTimeWithTimeZoneWrapper, } diff --git a/thoughts-frontend/app/globals.css b/thoughts-frontend/app/globals.css index a2dc41e..dc98be7 100644 --- a/thoughts-frontend/app/globals.css +++ b/thoughts-frontend/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/thoughts-frontend/app/page.tsx b/thoughts-frontend/app/page.tsx index d6c90fa..3d499cc 100644 --- a/thoughts-frontend/app/page.tsx +++ b/thoughts-frontend/app/page.tsx @@ -1,6 +1,10 @@ "use client"; import { useState, useEffect, FormEvent } from "react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardHeader, CardContent } from "@/components/ui/card"; +import { Alert } from "@/components/ui/alert"; interface Thought { id: number; @@ -10,16 +14,11 @@ interface Thought { } export default function Home() { - // State to store the list of thoughts for the feed const [thoughts, setThoughts] = useState([]); - // State for the content of the new thought being typed const [newThoughtContent, setNewThoughtContent] = useState(""); - // State to manage loading status const [isLoading, setIsLoading] = useState(true); - // State to hold any potential errors during API calls const [error, setError] = useState(null); - // Function to fetch the feed from the backend API const fetchFeed = async () => { try { setError(null); @@ -28,7 +27,6 @@ export default function Home() { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); - // The API returns { thoughts: [...] }, so we access the nested array setThoughts(data.thoughts || []); } catch (e: unknown) { console.error("Failed to fetch feed:", e); @@ -40,15 +38,13 @@ export default function Home() { } }; - // useEffect hook to fetch the feed when the component first loads useEffect(() => { fetchFeed(); }, []); - // Handler for submitting the new thought form const handleSubmitThought = async (e: FormEvent) => { e.preventDefault(); - if (!newThoughtContent.trim()) return; // Prevent empty posts + if (!newThoughtContent.trim()) return; try { const response = await fetch("http://localhost:8000/thoughts", { @@ -56,7 +52,6 @@ export default function Home() { headers: { "Content-Type": "application/json", }, - // We are hardcoding author_id: 1 as we don't have auth yet body: JSON.stringify({ content: newThoughtContent, author_id: 1 }), }); @@ -64,9 +59,7 @@ export default function Home() { throw new Error(`HTTP error! status: ${response.status}`); } - // Clear the input box setNewThoughtContent(""); - // Refresh the feed to show the new post fetchFeed(); } catch (e: unknown) { console.error("Failed to post thought:", e); @@ -91,12 +84,14 @@ export default function Home() { {/* New Thought Form */} -
+
-