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. +

+
+ +
+ +
{children}
+
+
+ ); +} 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 ( +
+
+

+ + {tagName} +

+
+
+ {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 ( +
+
+

Conversation

+
+
+ +
+
+ ); +} 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