diff --git a/thoughts-frontend/app/page.tsx b/thoughts-frontend/app/page.tsx
index c95f232..ebac652 100644
--- a/thoughts-frontend/app/page.tsx
+++ b/thoughts-frontend/app/page.tsx
@@ -7,7 +7,8 @@ import {
getTopFriends,
Me,
} from "@/lib/api";
-import { PostThoughtForm } from "@/components/post-thought-form";
+import { ThoughtForm } from "@/components/thought-form";
+import { EmptyState } from "@/components/empty-state";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { PopularTags } from "@/components/popular-tags";
@@ -80,7 +81,7 @@ async function FeedPage({
-
+
@@ -102,9 +103,7 @@ async function FeedPage({
/>
))}
{thoughtThreads.length === 0 && (
-
- Your feed is empty. Follow some users to see their thoughts!
-
+
)}
) : (
-
- No user found at {query}
-
+
)
) : results ? (
@@ -92,9 +91,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
) : (
-
- No results found or an error occurred.
-
+
)}
diff --git a/thoughts-frontend/app/tags/[tagName]/page.tsx b/thoughts-frontend/app/tags/[tagName]/page.tsx
index 94293e9..953df10 100644
--- a/thoughts-frontend/app/tags/[tagName]/page.tsx
+++ b/thoughts-frontend/app/tags/[tagName]/page.tsx
@@ -23,6 +23,7 @@ export async function generateMetadata({
},
};
}
+import { EmptyState } from "@/components/empty-state";
import { buildThoughtThreads } from "@/lib/utils";
import { ThoughtThread } from "@/components/thought-thread";
import { notFound } from "next/navigation";
@@ -66,9 +67,7 @@ export default async function TagPage({ params }: TagPageProps) {
/>
))}
{thoughtThreads.length === 0 && (
-
- No thoughts found for this tag.
-
+
)}
diff --git a/thoughts-frontend/app/users/[username]/page.tsx b/thoughts-frontend/app/users/[username]/page.tsx
index 1d2a745..bbb4ef1 100644
--- a/thoughts-frontend/app/users/[username]/page.tsx
+++ b/thoughts-frontend/app/users/[username]/page.tsx
@@ -44,6 +44,7 @@ export async function generateMetadata({
},
};
}
+import { EmptyState } from "@/components/empty-state";
import { UserAvatar } from "@/components/user-avatar";
import { Calendar, Settings } from "lucide-react";
import { Card } from "@/components/ui/card";
@@ -278,14 +279,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
/>
))}
{thoughtThreads.length === 0 && (
-
-
- This user hasn't posted any public thoughts yet.
-
-
+
)}
{isOwnProfile && (
diff --git a/thoughts-frontend/components/empty-state.tsx b/thoughts-frontend/components/empty-state.tsx
new file mode 100644
index 0000000..5dcc01d
--- /dev/null
+++ b/thoughts-frontend/components/empty-state.tsx
@@ -0,0 +1,12 @@
+interface EmptyStateProps {
+ message: string
+ className?: string
+}
+
+export function EmptyState({ message, className }: EmptyStateProps) {
+ return (
+
+ {message}
+
+ )
+}
diff --git a/thoughts-frontend/components/loading-skeleton.tsx b/thoughts-frontend/components/loading-skeleton.tsx
new file mode 100644
index 0000000..8b7cbbe
--- /dev/null
+++ b/thoughts-frontend/components/loading-skeleton.tsx
@@ -0,0 +1,34 @@
+import { Card, CardContent, CardHeader } from "@/components/ui/card"
+import { Skeleton } from "@/components/ui/skeleton"
+
+export function ThoughtSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ProfileSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/thoughts-frontend/components/post-thought-form.tsx b/thoughts-frontend/components/post-thought-form.tsx
deleted file mode 100644
index 124fa63..0000000
--- a/thoughts-frontend/components/post-thought-form.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-"use client";
-
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Form,
- FormField,
- FormItem,
- FormControl,
- FormMessage,
-} from "@/components/ui/form";
-import { Textarea } from "@/components/ui/textarea";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { CreateThoughtSchema, createThought } from "@/lib/api";
-import { useAuth } from "@/hooks/use-auth";
-import { toast } from "sonner";
-import { Globe, Lock, Users } from "lucide-react";
-import { useState } from "react";
-import { Confetti } from "./confetti";
-
-export function PostThoughtForm() {
- const router = useRouter();
- const { token } = useAuth();
- const [showConfetti, setShowConfetti] = useState(false);
-
- const form = useForm>({
- resolver: zodResolver(CreateThoughtSchema),
- defaultValues: { content: "", visibility: "public" },
- });
-
- async function onSubmit(values: z.infer) {
- if (!token) {
- toast.error("You must be logged in to post.");
- return;
- }
-
- try {
- await createThought(values, token);
- toast.success("Your thought has been posted!");
- setShowConfetti(true);
- form.reset();
- router.refresh(); // This is the key to updating the feed
- } catch {
- toast.error("Failed to post thought. Please try again.");
- }
- }
-
- return (
- <>
- setShowConfetti(false)} />
-
-
-
-
-
-
- >
- );
-}
diff --git a/thoughts-frontend/components/reply-form.tsx b/thoughts-frontend/components/reply-form.tsx
deleted file mode 100644
index 12076c7..0000000
--- a/thoughts-frontend/components/reply-form.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-"use client";
-
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import {
- Form,
- FormField,
- FormItem,
- FormControl,
- FormMessage,
-} from "@/components/ui/form";
-import { Textarea } from "@/components/ui/textarea";
-import { CreateThoughtSchema, createThought } from "@/lib/api";
-import { useAuth } from "@/hooks/use-auth";
-import { toast } from "sonner";
-import { useState } from "react";
-import { Confetti } from "./confetti";
-
-interface ReplyFormProps {
- parentThoughtId: string;
- onReplySuccess: () => void; // A callback to close the form after success
-}
-
-export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
- const router = useRouter();
- const { token } = useAuth();
- const [showConfetti, setShowConfetti] = useState(false);
-
- const form = useForm>({
- resolver: zodResolver(CreateThoughtSchema),
- defaultValues: {
- content: "",
- inReplyToId: parentThoughtId,
- visibility: "public",
- },
- });
-
- async function onSubmit(values: z.infer) {
- if (!token) {
- toast.error("You must be logged in to reply.");
- return;
- }
-
- try {
- await createThought(values, token);
- toast.success("Your reply has been posted!");
- form.reset();
- setShowConfetti(true);
- console.log("Showing confetti");
- onReplySuccess();
- router.refresh();
- } catch {
- toast.error("Failed to post reply. Please try again.");
- }
- }
-
- return (
- <>
- setShowConfetti(false)} />
-
-
- >
- );
-}
diff --git a/thoughts-frontend/components/thought-card.tsx b/thoughts-frontend/components/thought-card.tsx
index 8373d05..cbfe1f4 100644
--- a/thoughts-frontend/components/thought-card.tsx
+++ b/thoughts-frontend/components/thought-card.tsx
@@ -36,7 +36,7 @@ import {
MoreHorizontal,
Trash2,
} from "lucide-react";
-import { ReplyForm } from "@/components/reply-form";
+import { ThoughtForm } from "@/components/thought-form";
import Link from "next/link";
import { cn } from "@/lib/utils";
@@ -195,9 +195,9 @@ export function ThoughtCard({
{isReplyOpen && (
- setIsReplyOpen(false)}
+ setIsReplyOpen(false)}
/>
)}
diff --git a/thoughts-frontend/components/thought-form.tsx b/thoughts-frontend/components/thought-form.tsx
new file mode 100644
index 0000000..bf1c490
--- /dev/null
+++ b/thoughts-frontend/components/thought-form.tsx
@@ -0,0 +1,142 @@
+"use client"
+
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent } from "@/components/ui/card"
+import {
+ Form,
+ FormField,
+ FormItem,
+ FormControl,
+ FormMessage,
+} from "@/components/ui/form"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { CreateThoughtSchema } from "@/lib/api"
+import { useAuth } from "@/hooks/use-auth"
+import { toast } from "sonner"
+import { Globe, Lock, Users } from "lucide-react"
+import { useState } from "react"
+import { Confetti } from "./confetti"
+import { createThought } from "@/app/actions/thoughts"
+
+interface ThoughtFormProps {
+ /** Set to the parent thought ID when composing a reply. */
+ replyToId?: string
+ /** Called after successful submit (e.g. close the reply panel). */
+ onSuccess?: () => void
+ /** Whether to wrap in a Card. Defaults to true when no replyToId. */
+ card?: boolean
+}
+
+export function ThoughtForm({ replyToId, onSuccess, card = !replyToId }: ThoughtFormProps) {
+ const { token } = useAuth()
+ const [showConfetti, setShowConfetti] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(CreateThoughtSchema),
+ defaultValues: {
+ content: "",
+ visibility: "public",
+ ...(replyToId ? { inReplyToId: replyToId } : {}),
+ },
+ })
+
+ async function onSubmit(values: z.infer) {
+ if (!token) {
+ toast.error("You must be logged in.")
+ return
+ }
+ try {
+ await createThought(values)
+ toast.success(replyToId ? "Reply posted!" : "Thought posted!")
+ setShowConfetti(true)
+ form.reset()
+ onSuccess?.()
+ } catch {
+ toast.error(replyToId ? "Failed to post reply." : "Failed to post thought.")
+ }
+ }
+
+ const inner = (
+
+
+ )
+
+ return (
+ <>
+ setShowConfetti(false)} />
+ {card
+ ? {inner}
+ : {inner}
+ }
+ >
+ )
+}