diff --git a/thoughts-frontend/app/settings/layout.tsx b/thoughts-frontend/app/settings/layout.tsx
new file mode 100644
index 0000000..dd58833
--- /dev/null
+++ b/thoughts-frontend/app/settings/layout.tsx
@@ -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 (
+
+
+
Settings
+
+ Manage your account settings and profile.
+
+
+
+
+
+ );
+}
diff --git a/thoughts-frontend/app/settings/profile/page.tsx b/thoughts-frontend/app/settings/profile/page.tsx
index cfab14d..efb1067 100644
--- a/thoughts-frontend/app/settings/profile/page.tsx
+++ b/thoughts-frontend/app/settings/profile/page.tsx
@@ -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 (
-
-
- Edit Profile
-
- Update your public profile information.
-
-
-
-
+
+
Profile
+
+ This is how others will see you on the site.
+
+
+
);
}
diff --git a/thoughts-frontend/app/tags/[tagName]/page.tsx b/thoughts-frontend/app/tags/[tagName]/page.tsx
new file mode 100644
index 0000000..1892d26
--- /dev/null
+++ b/thoughts-frontend/app/tags/[tagName]/page.tsx
@@ -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(
+ userProfiles
+ .filter((u): u is User => !!u)
+ .map((user) => [user.username, { avatarUrl: user.avatarUrl }])
+ );
+
+ const { topLevelThoughts, repliesByParentId } =
+ buildThoughtThreads(allThoughts);
+
+ return (
+
+
+
+ {topLevelThoughts.map((thought) => (
+
+ ))}
+ {topLevelThoughts.length === 0 && (
+
+ No thoughts found for this tag.
+
+ )}
+
+
+ );
+}
diff --git a/thoughts-frontend/app/thoughts/[thoughtId]/page.tsx b/thoughts-frontend/app/thoughts/[thoughtId]/page.tsx
new file mode 100644
index 0000000..4499496
--- /dev/null
+++ b/thoughts-frontend/app/thoughts/[thoughtId]/page.tsx
@@ -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 {
+ 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();
+ if (author) {
+ authorDetails.set(author.username, { avatarUrl: author.avatarUrl });
+ }
+
+ const { repliesByParentId } = buildThoughtThreads(allThoughts);
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/thoughts-frontend/components/edit-profile-form.tsx b/thoughts-frontend/components/edit-profile-form.tsx
index d38a55e..c3a7f08 100644
--- a/thoughts-frontend/components/edit-profile-form.tsx
+++ b/thoughts-frontend/components/edit-profile-form.tsx
@@ -51,7 +51,7 @@ export function EditProfileForm({ currentUser }: EditProfileFormProps) {
router.push(`/users/${currentUser.username}`);
router.refresh(); // Ensure fresh data is loaded
} catch (err) {
- toast.error("Failed to update profile.");
+ toast.error(`Failed to update profile. ${err}`);
}
}
diff --git a/thoughts-frontend/components/header.tsx b/thoughts-frontend/components/header.tsx
index 805b18d..fd7cf24 100644
--- a/thoughts-frontend/components/header.tsx
+++ b/thoughts-frontend/components/header.tsx
@@ -6,7 +6,6 @@ import { Button } from "./ui/button";
import { UserNav } from "./user-nav";
import { MainNav } from "./main-nav";
import { ThemeToggle } from "./theme-toggle";
-import { Wind } from "lucide-react";
export function Header() {
const { token } = useAuth();
diff --git a/thoughts-frontend/components/settings-nav.tsx b/thoughts-frontend/components/settings-nav.tsx
new file mode 100644
index 0000000..e5f4833
--- /dev/null
+++ b/thoughts-frontend/components/settings-nav.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+interface SettingsNavProps extends React.HTMLAttributes {
+ items: {
+ href: string;
+ title: string;
+ }[];
+}
+
+export function SettingsNav({ className, items, ...props }: SettingsNavProps) {
+ const pathname = usePathname();
+
+ return (
+
+ );
+}
diff --git a/thoughts-frontend/components/thought-card.tsx b/thoughts-frontend/components/thought-card.tsx
index 0b6bfd3..b2b4062 100644
--- a/thoughts-frontend/components/thought-card.tsx
+++ b/thoughts-frontend/components/thought-card.tsx
@@ -72,6 +72,7 @@ export function ThoughtCard({
toast.success("Thought deleted successfully.");
router.refresh();
} catch (error) {
+ console.error("Failed to delete thought:", error);
toast.error("Failed to delete thought.");
} finally {
setIsAlertOpen(false);
diff --git a/thoughts-frontend/lib/api.ts b/thoughts-frontend/lib/api.ts
index 3ad8d48..43bd55a 100644
--- a/thoughts-frontend/lib/api.ts
+++ b/thoughts-frontend/lib/api.ts
@@ -192,4 +192,20 @@ export const updateProfile = (
},
UserSchema, // Expect the updated user object back
token
+ );
+
+ export const getThoughtsByTag = (tagName: string, token: string | null) =>
+ apiFetch(
+ `/tags/${tagName}`,
+ {},
+ z.object({ thoughts: z.array(ThoughtSchema) }),
+ token
+ );
+
+export const getThoughtById = (thoughtId: string, token: string | null) =>
+ apiFetch(
+ `/thoughts/${thoughtId}`,
+ {},
+ ThoughtSchema, // Expect a single thought object
+ token
);
\ No newline at end of file