mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
134 lines
5.2 KiB
TypeScript
134 lines
5.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { data } from 'react-router';
|
|
import RouteErrorState from '../../../components/ui/error/RouteErrorState';
|
|
import { getOptionalAuth } from '../../auth/auth.server';
|
|
import { getPublicUserProfile, type PublicUserProfile } from '../users.server';
|
|
import ProfileAvatar from '../components/profile/ProfileAvatar';
|
|
import ProfileSidebarCard from '../components/profile/ProfileSidebarCard';
|
|
import ProfileTabs, { type ProfileTab } from '../components/profile/ProfileTabs';
|
|
import ActivityTab from '../components/profile/tabs/ActivityTab';
|
|
import FollowingTab from '../components/profile/tabs/FollowingTab';
|
|
import LikedTab from '../components/profile/tabs/LikedTab';
|
|
import {
|
|
FILLER_ACTIVITY,
|
|
FILLER_FOLLOWING,
|
|
FILLER_LIKED,
|
|
FILLER_PROFILE_META,
|
|
} from '../utils/filler-user-profile';
|
|
import type { Route } from './+types/user-profile';
|
|
|
|
export const meta = ({ loaderData }: Route.MetaArgs) => [
|
|
{
|
|
title: loaderData
|
|
? `${loaderData.profile.username} | The Biergarten App`
|
|
: 'Profile | The Biergarten App',
|
|
},
|
|
];
|
|
|
|
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|
const auth = await getOptionalAuth(request);
|
|
|
|
const profile = await getPublicUserProfile(params.id, auth?.accessToken);
|
|
if (!profile) {
|
|
throw data('User not found.', { status: 404, statusText: 'Not Found' });
|
|
}
|
|
|
|
return {
|
|
profile,
|
|
isOwnProfile: auth?.userAccountId === profile.userAccountId,
|
|
};
|
|
};
|
|
|
|
const joinedLabel = (isoDate: string) =>
|
|
`Joined ${new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(new Date(isoDate))}`;
|
|
|
|
interface ProfileContentProps {
|
|
profile: PublicUserProfile;
|
|
isOwnProfile: boolean;
|
|
}
|
|
|
|
/**
|
|
* Owns the profile-scoped interactive state. Keyed by `profile.userAccountId` in the
|
|
* parent so React remounts it (resetting follow/tab state) when the route's `:id` changes,
|
|
* since React Router reuses the route component itself across param-only navigations.
|
|
*/
|
|
const ProfileContent = ({ profile, isOwnProfile }: ProfileContentProps) => {
|
|
const [activeTab, setActiveTab] = useState<ProfileTab>('activity');
|
|
const [isFollowingProfile, setIsFollowingProfile] = useState(false);
|
|
const [following, setFollowing] = useState(() =>
|
|
FILLER_FOLLOWING.map((f) => ({ ...f, isFollowing: true })),
|
|
);
|
|
|
|
const handleToggleFollowRow = (id: string) => {
|
|
setFollowing((prev) =>
|
|
prev.map((f) => (f.id === id ? { ...f, isFollowing: !f.isFollowing } : f)),
|
|
);
|
|
};
|
|
|
|
const displayName = `${profile.firstName} ${profile.lastName}`.trim() || profile.username;
|
|
|
|
const stats = [
|
|
{ label: 'Following', value: FILLER_PROFILE_META.following },
|
|
{ label: 'Followers', value: FILLER_PROFILE_META.followers },
|
|
{ label: 'Breweries visited', value: FILLER_PROFILE_META.breweriesVisited },
|
|
{ label: 'Ratings given', value: FILLER_PROFILE_META.ratingsGiven },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-base-200 pb-16">
|
|
<div className="mx-auto max-w-7xl px-5 pt-10">
|
|
<div className="rounded-box h-[190px] bg-base-300" />
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-8 items-start -mt-12">
|
|
<div className="lg:sticky lg:top-8 flex flex-col gap-5 px-2">
|
|
<ProfileAvatar avatarUrl={profile.avatarUrl} displayName={displayName} />
|
|
|
|
<ProfileSidebarCard
|
|
displayName={displayName}
|
|
username={profile.username}
|
|
bio={profile.biography}
|
|
location={FILLER_PROFILE_META.location}
|
|
joinedLabel={joinedLabel(profile.createdAt)}
|
|
stats={stats}
|
|
isOwnProfile={isOwnProfile}
|
|
isFollowing={isFollowingProfile}
|
|
onToggleFollow={() => setIsFollowingProfile((prev) => !prev)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-12">
|
|
<ProfileTabs
|
|
activeTab={activeTab}
|
|
onChange={setActiveTab}
|
|
panels={{
|
|
activity: <ActivityTab activity={FILLER_ACTIVITY} />,
|
|
following: (
|
|
<FollowingTab
|
|
following={following}
|
|
onToggleFollow={handleToggleFollowRow}
|
|
/>
|
|
),
|
|
liked: <LikedTab liked={FILLER_LIKED} />,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const UserProfile = ({ loaderData }: Route.ComponentProps) => {
|
|
const { profile, isOwnProfile } = loaderData;
|
|
|
|
return (
|
|
<ProfileContent key={profile.userAccountId} profile={profile} isOwnProfile={isOwnProfile} />
|
|
);
|
|
};
|
|
|
|
export default UserProfile;
|
|
|
|
export const ErrorBoundary = ({ error }: Route.ErrorBoundaryProps) => (
|
|
<RouteErrorState error={error} />
|
|
);
|