diff --git a/thoughts-frontend/app/(auth)/layout.tsx b/thoughts-frontend/app/(auth)/layout.tsx new file mode 100644 index 0000000..4867b37 --- /dev/null +++ b/thoughts-frontend/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +// app/(auth)/layout.tsx +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/thoughts-frontend/app/(auth)/login/page.tsx b/thoughts-frontend/app/(auth)/login/page.tsx new file mode 100644 index 0000000..c5edd91 --- /dev/null +++ b/thoughts-frontend/app/(auth)/login/page.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { LoginSchema, loginUser } from "@/lib/api"; +import { useAuth } from "@/hooks/use-auth"; +import { useState } from "react"; + +export default function LoginPage() { + const router = useRouter(); + const { setToken } = useAuth(); + const [error, setError] = useState(null); + + const form = useForm>({ + resolver: zodResolver(LoginSchema), + defaultValues: { username: "", password: "" }, + }); + + async function onSubmit(values: z.infer) { + try { + setError(null); + const { token } = await loginUser(values); + setToken(token); + router.push("/"); // Redirect to homepage on successful login + } catch (err) { + setError("Invalid username or password."); + } + } + + return ( + + + Login + + Enter your credentials to access your account. + + + +
+ + {/* ... Form fields for username and password ... */} + ( + + Username + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + {error && ( +

{error}

+ )} + + + +

+ Don't have an account?{" "} + + Register + +

+
+
+ ); +} diff --git a/thoughts-frontend/app/(auth)/register/page.tsx b/thoughts-frontend/app/(auth)/register/page.tsx new file mode 100644 index 0000000..0652aa3 --- /dev/null +++ b/thoughts-frontend/app/(auth)/register/page.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { RegisterSchema, registerUser } from "@/lib/api"; +import { useState } from "react"; + +export default function RegisterPage() { + const router = useRouter(); + const [error, setError] = useState(null); + + const form = useForm>({ + resolver: zodResolver(RegisterSchema), + defaultValues: { username: "", email: "", password: "" }, + }); + + async function onSubmit(values: z.infer) { + try { + setError(null); + await registerUser(values); + // You can automatically log the user in here or just redirect them + router.push("/login"); + } catch (err) { + setError("Username or email may already be taken."); + } + } + + return ( + + + Create an Account + Enter your details to register. + + +
+ + {/* ... Form fields for username, email, and password ... */} + ( + + Username + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + {error && ( +

{error}

+ )} + + + +

+ Already have an account?{" "} + + Login + +

+
+
+ ); +} diff --git a/thoughts-frontend/app/layout.tsx b/thoughts-frontend/app/layout.tsx index f7fa87e..8cf5f6e 100644 --- a/thoughts-frontend/app/layout.tsx +++ b/thoughts-frontend/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { AuthProvider } from "@/hooks/use-auth"; +import { Toaster } from "@/components/ui/sonner"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,7 +29,10 @@ export default function RootLayout({ - {children} + + {children} + + ); diff --git a/thoughts-frontend/app/page.tsx b/thoughts-frontend/app/page.tsx index 3d499cc..0ef49b5 100644 --- a/thoughts-frontend/app/page.tsx +++ b/thoughts-frontend/app/page.tsx @@ -1,155 +1,83 @@ -"use client"; - -import { useState, useEffect, FormEvent } from "react"; +// app/page.tsx +import { cookies } from "next/headers"; +import { getFeed, getUserProfile } from "@/lib/api"; +import { ThoughtCard } from "@/components/thought-card"; +import { PostThoughtForm } from "@/components/post-thought-form"; import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { Card, CardHeader, CardContent } from "@/components/ui/card"; -import { Alert } from "@/components/ui/alert"; +import Link from "next/link"; -interface Thought { - id: number; - author_id: number; - content: string; - created_at: string; +// This is now an async Server Component +export default async function Home() { + const token = (await cookies()).get("auth_token")?.value ?? null; + + if (token) { + return ; + } else { + return ; + } } -export default function Home() { - const [thoughts, setThoughts] = useState([]); - const [newThoughtContent, setNewThoughtContent] = useState(""); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); +async function FeedPage({ token }: { token: string }) { + const feedData = await getFeed(token); + const authors = [...new Set(feedData.thoughts.map((t) => t.authorUsername))]; + const userProfiles = await Promise.all( + authors.map((username) => getUserProfile(username, token).catch(() => null)) + ); - const fetchFeed = async () => { - try { - setError(null); - const response = await fetch("http://localhost:8000/feed"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - setThoughts(data.thoughts || []); - } catch (e: unknown) { - console.error("Failed to fetch feed:", e); - setError( - "Could not load the feed. The backend might be busy. Please try refreshing." - ); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - fetchFeed(); - }, []); - - const handleSubmitThought = async (e: FormEvent) => { - e.preventDefault(); - if (!newThoughtContent.trim()) return; - - try { - const response = await fetch("http://localhost:8000/thoughts", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ content: newThoughtContent, author_id: 1 }), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - setNewThoughtContent(""); - fetchFeed(); - } catch (e: unknown) { - console.error("Failed to post thought:", e); - setError("Failed to post your thought. Please try again."); - } - }; + const authorDetails = new Map( + userProfiles + .filter(Boolean) + .map((user) => [user!.username, { avatarUrl: user!.avatarUrl }]) + ); return ( -
-
- {/* Header */} -
-

- Thoughts -

-

- Your space on the decentralized web. +

+
+

Your Feed

+
+
+ + {feedData.thoughts.map((thought) => ( + + ))} + {feedData.thoughts.length === 0 && ( +

+ Your feed is empty. Follow some users to see their thoughts here!

-
+ )} + +
+ ); +} - {/* New Thought Form */} - -
-