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.
This commit is contained in:
30
thoughts-frontend/app/users/[username]/loading.tsx
Normal file
30
thoughts-frontend/app/users/[username]/loading.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
// app/users/[username]/loading.tsx
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
// This is the ProfileSkeleton component from the previous step.
|
||||
// Next.js will automatically render this while page.tsx is loading.
|
||||
export default function ProfileLoading() {
|
||||
return (
|
||||
<div>
|
||||
<Skeleton className="h-48 w-full" />
|
||||
<main className="container mx-auto max-w-3xl p-4 -mt-16">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-end gap-4">
|
||||
<Skeleton className="h-24 w-24 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-40" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="h-6 w-full mt-4" />
|
||||
<Skeleton className="h-6 w-3/4 mt-2" />
|
||||
</Card>
|
||||
<div className="mt-8 space-y-4">
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
86
thoughts-frontend/app/users/[username]/page.tsx
Normal file
86
thoughts-frontend/app/users/[username]/page.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { getUserProfile, getUserThoughts } from "@/lib/api";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { ThoughtCard } from "@/components/thought-card";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
interface ProfilePageProps {
|
||||
params: { username: string };
|
||||
}
|
||||
|
||||
export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||
const { username } = params;
|
||||
|
||||
// Fetch data directly on the server.
|
||||
// The `loading.tsx` file will be shown to the user during this fetch.
|
||||
const [userResult, thoughtsResult] = await Promise.allSettled([
|
||||
getUserProfile(username),
|
||||
getUserThoughts(username),
|
||||
]);
|
||||
|
||||
// Handle errors from the server-side fetch
|
||||
if (userResult.status === "rejected") {
|
||||
// If the user isn't found, render the Next.js 404 page
|
||||
notFound();
|
||||
}
|
||||
|
||||
const user = userResult.value;
|
||||
const thoughts =
|
||||
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.thoughts : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Custom CSS Injection */}
|
||||
{user.customCss && (
|
||||
<style dangerouslySetInnerHTML={{ __html: user.customCss }} />
|
||||
)}
|
||||
|
||||
{/* Header Image */}
|
||||
<div
|
||||
className="h-48 bg-gray-200 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: user.headerUrl ? `url(${user.headerUrl})` : "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<main className="container mx-auto max-w-3xl p-4 -mt-16">
|
||||
{/* Profile Info */}
|
||||
<Card className="p-6 bg-card/80 backdrop-blur-lg">
|
||||
<div className="flex items-end gap-4">
|
||||
<div className="w-24 h-24 rounded-full border-4 border-background">
|
||||
<UserAvatar src={user.avatarUrl} alt={user.displayName} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{user.displayName || user.username}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">@{user.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 whitespace-pre-wrap">{user.bio}</p>
|
||||
<div className="flex items-center gap-2 mt-4 text-sm text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Joined {new Date(user.joinedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Thoughts Feed */}
|
||||
<div className="mt-8 space-y-4">
|
||||
{thoughts.map((thought) => (
|
||||
<ThoughtCard
|
||||
key={thought.id}
|
||||
thought={thought}
|
||||
author={{ username: user.username, avatarUrl: user.avatarUrl }}
|
||||
/>
|
||||
))}
|
||||
{thoughts.length === 0 && (
|
||||
<p className="text-center text-muted-foreground">
|
||||
This user hasn't posted any thoughts yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user