feat: implement settings layout and navigation, add tag and thought pages with API integration

This commit is contained in:
2025-09-06 21:56:41 +02:00
parent 5344e0d6a8
commit 85e3425d4b
9 changed files with 257 additions and 18 deletions

View File

@@ -0,0 +1,35 @@
// app/settings/layout.tsx
import { SettingsNav } from "@/components/settings-nav";
import { Separator } from "@/components/ui/separator";
const sidebarNavItems = [
{
title: "Profile",
href: "/settings/profile",
},
// You can add more links here later, e.g., "Account", "API Keys"
];
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="container mx-auto max-w-5xl space-y-6 p-10 pb-16">
<div className="space-y-0.5">
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
<p className="text-muted-foreground">
Manage your account settings and profile.
</p>
</div>
<Separator className="my-6" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside className="-mx-4 lg:w-1/5">
<SettingsNav items={sidebarNavItems} />
</aside>
<div className="flex-1 lg:max-w-2xl">{children}</div>
</div>
</div>
);
}

View File

@@ -1,15 +1,9 @@
// app/settings/profile/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getMe } from "@/lib/api";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { EditProfileForm } from "@/components/edit-profile-form";
// This is a Server Component to fetch initial data
export default async function EditProfilePage() {
const token = (await cookies()).get("auth_token")?.value;
@@ -25,15 +19,13 @@ export default async function EditProfilePage() {
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Edit Profile</CardTitle>
<CardDescription>
Update your public profile information.
</CardDescription>
</CardHeader>
<EditProfileForm currentUser={me} />
</Card>
<div>
<h3 className="text-lg font-medium">Profile</h3>
<p className="text-sm text-muted-foreground">
This is how others will see you on the site.
</p>
</div>
<EditProfileForm currentUser={me} />
</div>
);
}

View File

@@ -0,0 +1,68 @@
// app/tags/[tagName]/page.tsx
import { cookies } from "next/headers";
import { getThoughtsByTag, getUserProfile, getMe, Me, User } from "@/lib/api";
import { buildThoughtThreads } from "@/lib/utils";
import { ThoughtThread } from "@/components/thought-thread";
import { notFound } from "next/navigation";
import { Hash } from "lucide-react";
interface TagPageProps {
params: { tagName: string };
}
export default async function TagPage({ params }: TagPageProps) {
const { tagName } = params;
const token = (await cookies()).get("auth_token")?.value ?? null;
const [thoughtsResult, meResult] = await Promise.allSettled([
getThoughtsByTag(tagName, token),
token ? getMe(token) : Promise.resolve(null),
]);
if (thoughtsResult.status === "rejected") {
notFound();
}
const allThoughts = thoughtsResult.value.thoughts;
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
const authors = [...new Set(allThoughts.map((t) => t.authorUsername))];
const userProfiles = await Promise.all(
authors.map((username) => getUserProfile(username, token).catch(() => null))
);
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
userProfiles
.filter((u): u is User => !!u)
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
);
const { topLevelThoughts, repliesByParentId } =
buildThoughtThreads(allThoughts);
return (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
<h1 className="flex items-center gap-2 text-3xl font-bold">
<Hash className="h-7 w-7" />
{tagName}
</h1>
</header>
<main className="space-y-6">
{topLevelThoughts.map((thought) => (
<ThoughtThread
key={thought.id}
thought={thought}
repliesByParentId={repliesByParentId}
authorDetails={authorDetails}
currentUser={me}
/>
))}
{topLevelThoughts.length === 0 && (
<p className="text-center text-muted-foreground pt-8">
No thoughts found for this tag.
</p>
)}
</main>
</div>
);
}

View File

@@ -0,0 +1,85 @@
import { cookies } from "next/headers";
import {
getThoughtById,
getUserThoughts,
getUserProfile,
getMe,
Me,
Thought,
} from "@/lib/api";
import { buildThoughtThreads } from "@/lib/utils";
import { ThoughtThread } from "@/components/thought-thread";
import { notFound } from "next/navigation";
interface ThoughtPageProps {
params: { thoughtId: string };
}
async function findConversationRoot(
startThought: Thought,
token: string | null
): Promise<Thought> {
let currentThought = startThought;
while (currentThought.replyToId) {
const parentThought = await getThoughtById(
currentThought.replyToId,
token
).catch(() => null);
if (!parentThought) break;
currentThought = parentThought;
}
return currentThought;
}
export default async function ThoughtPage({ params }: ThoughtPageProps) {
const { thoughtId } = params;
const token = (await cookies()).get("auth_token")?.value ?? null;
const initialThought = await getThoughtById(thoughtId, token).catch(
() => null
);
if (!initialThought) {
notFound();
}
const rootThought = await findConversationRoot(initialThought, token);
const [thoughtsResult, meResult] = await Promise.allSettled([
getUserThoughts(rootThought.authorUsername, token),
token ? getMe(token) : Promise.resolve(null),
]);
if (thoughtsResult.status === "rejected") {
notFound();
}
const allThoughts = thoughtsResult.value.thoughts;
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
const author = await getUserProfile(rootThought.authorUsername, token).catch(
() => null
);
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
if (author) {
authorDetails.set(author.username, { avatarUrl: author.avatarUrl });
}
const { repliesByParentId } = buildThoughtThreads(allThoughts);
return (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
<h1 className="text-3xl font-bold">Conversation</h1>
</header>
<main>
<ThoughtThread
thought={rootThought}
repliesByParentId={repliesByParentId}
authorDetails={authorDetails}
currentUser={me}
/>
</main>
</div>
);
}