feat(18-05): add public profile page and setup visibility toggle

- Create public profile page at /users/$userId with avatar, name, bio, setups
- Create PublicSetupCard component for profile page setup listing
- Add isPublic toggle button on setup detail page
- Add Public badge to SetupCard in list view
- Update useSetups hook with isPublic field on interfaces
This commit is contained in:
2026-04-05 13:19:36 +02:00
parent f120d179f7
commit a9956681ba
6 changed files with 179 additions and 2 deletions

View File

@@ -0,0 +1,102 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { PublicSetupCard } from "../../components/PublicSetupCard";
import { usePublicProfile } from "../../hooks/useProfile";
export const Route = createFileRoute("/users/$userId")({
component: PublicProfilePage,
});
function PublicProfilePage() {
const { userId } = Route.useParams();
const numericId = Number(userId);
const { data: profile, isLoading, isError } = usePublicProfile(numericId);
if (isLoading) {
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div className="animate-pulse space-y-6">
<div className="flex items-center gap-4">
<div className="w-20 h-20 rounded-full bg-gray-200" />
<div className="space-y-2">
<div className="h-6 bg-gray-200 rounded w-40" />
<div className="h-4 bg-gray-200 rounded w-64" />
</div>
</div>
<div className="h-4 bg-gray-200 rounded w-24" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="h-24 bg-gray-200 rounded-xl" />
<div className="h-24 bg-gray-200 rounded-xl" />
</div>
</div>
</div>
);
}
if (isError || !profile) {
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<p className="text-gray-500">User not found.</p>
<Link
to="/"
className="text-sm text-gray-500 hover:text-gray-700 mt-4 inline-block"
>
&larr; Back to home
</Link>
</div>
);
}
const displayName = profile.displayName || `User #${profile.id}`;
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
{/* Profile header */}
<div className="flex items-center gap-5 mb-8">
{profile.avatarUrl ? (
<img
src={`/uploads/${profile.avatarUrl}`}
alt={displayName}
className="w-20 h-20 rounded-full object-cover"
/>
) : (
<div className="w-20 h-20 rounded-full bg-gray-100 flex items-center justify-center">
<svg
className="w-10 h-10 text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</div>
)}
<div>
<h1 className="text-xl font-semibold text-gray-900">{displayName}</h1>
{profile.bio && (
<p className="text-sm text-gray-500 mt-1 max-w-md">{profile.bio}</p>
)}
</div>
</div>
{/* Public setups */}
<div>
<h2 className="text-base font-medium text-gray-900 mb-4">
Public Setups
</h2>
{profile.setups.length === 0 ? (
<p className="text-sm text-gray-400 py-8 text-center">
No public setups yet
</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{profile.setups.map((setup) => (
<PublicSetupCard key={setup.id} setup={setup} />
))}
</div>
)}
</div>
</div>
);
}