import { sdk } from "@farcaster/miniapp-sdk"; import { useEffect, useState } from "react"; import { ShareSheet } from "./components/ShareSheet"; import { useAI } from "./hooks/useAI"; import { type GuestbookEntry, useAddGuestbookEntry, useGuestbook } from "./hooks/useGuestbook"; interface Notification { type: string; most_recent_timestamp: string; seen: boolean; cast?: { text: string; hash: string; author: { username: string; display_name: string; pfp_url: string; }; }; follows?: Array<{ user: { username: string; display_name: string; pfp_url: string; }; }>; reactions?: Array<{ user: { username: string; display_name: string; pfp_url: string; }; }>; count?: number; } interface NotificationsResponse { notifications: Notification[]; unseen_notifications_count: number; } export default function App() { const [isReady, setIsReady] = useState(false); const [context, setContext] = useState(null); const [activeTab, setActiveTab] = useState<"notifications" | "guestbook" | "ai">("guestbook"); // Notifications state const [notifications, setNotifications] = useState([]); const [unseenCount, setUnseenCount] = useState(0); const [notifLoading, setNotifLoading] = useState(false); const [notifError, setNotifError] = useState(null); // Guestbook hooks const { data: guestbookEntries, isLoading: guestbookLoading, error: guestbookError, } = useGuestbook(); const addEntry = useAddGuestbookEntry(); const [newMessage, setNewMessage] = useState(""); const [showShareSheet, setShowShareSheet] = useState(false); const [lastSignedMessage, setLastSignedMessage] = useState(""); // AI state const [aiLoading, setAiLoading] = useState(false); const [aiError, setAiError] = useState(null); const [aiResult, setAiResult] = useState<{ category: string; reason: string } | null>(null); const resetAI = () => { setAiError(null); setAiResult(null); }; // Cast idea generator (demonstrates useAI hook) const { generate, isLoading: castIdeaLoading, error: castIdeaError, reset: resetCastIdea, } = useAI(); const [castIdea, setCastIdea] = useState(null); useEffect(() => { const init = async () => { const ctx = await sdk.context; setContext(ctx); sdk.actions.ready(); setIsReady(true); }; init(); }, []); const fetchNotifications = async (fid: number) => { setNotifLoading(true); setNotifError(null); try { const response = await fetch(`/api/notifications?fid=${fid}`); if (!response.ok) throw new Error("Failed to fetch notifications"); const data: NotificationsResponse = await response.json(); setNotifications(data.notifications || []); setUnseenCount(data.unseen_notifications_count || 0); } catch (err) { setNotifError(err instanceof Error ? err.message : "Unknown error"); } finally { setNotifLoading(false); } }; const handleAddGuestbookEntry = async () => { if (!context?.user || !newMessage.trim()) return; const message = newMessage.trim(); try { await addEntry.mutateAsync({ fid: context.user.fid, username: context.user.username, displayName: context.user.displayName, pfpUrl: context.user.pfpUrl, message, }); setNewMessage(""); setLastSignedMessage(message); setShowShareSheet(true); } catch (err) { console.error("Failed to add entry:", err); } }; // Build the share embed URL - points to /share page with fc:miniapp meta tags // This creates a clickable miniapp card in the cast, not just an image const getShareEmbedUrl = () => { if (!context?.user) return window.location.origin; const params = new URLSearchParams({ username: context.user.username, displayName: context.user.displayName || context.user.username, message: lastSignedMessage, appName: "jack-template", }); if (context.user.pfpUrl) { params.set("pfpUrl", context.user.pfpUrl); } return `${window.location.origin}/share?${params.toString()}`; }; const analyzeProfile = async () => { if (!context?.user) return; resetAI(); setAiResult(null); setAiLoading(true); try { // Call dedicated profile analysis endpoint // It fetches user data + recent casts from Neynar, then runs AI const res = await fetch("/api/ai/analyze-profile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fid: context.user.fid }), }); if (!res.ok) { const errorData = (await res.json()) as { error?: string }; throw new Error(errorData.error || "Analysis failed"); } const result = (await res.json()) as { category: string; reason: string }; setAiResult({ category: result.category, reason: result.reason }); } catch (err) { console.error("Failed to analyze profile:", err); setAiError(err instanceof Error ? err.message : "Analysis failed"); } finally { setAiLoading(false); } }; const generateCastIdea = async () => { resetCastIdea(); setCastIdea(null); try { const result = await generate( "Generate a creative, engaging Farcaster cast idea (under 320 chars). Topic: something interesting about web3, crypto culture, or tech. Be witty and conversational. Return ONLY the cast text, no quotes or explanation.", ); setCastIdea(result.result); } catch (err) { console.error("Failed to generate cast idea:", err); } }; const copyCastIdea = async () => { if (!castIdea) return; try { await navigator.clipboard.writeText(castIdea); } catch (err) { console.error("Failed to copy:", err); } }; const formatTime = (timestamp: string) => { const diff = Date.now() - new Date(timestamp).getTime(); const mins = Math.floor(diff / 60000); const hrs = Math.floor(mins / 60); const days = Math.floor(hrs / 24); if (days > 0) return `${days}d`; if (hrs > 0) return `${hrs}h`; if (mins > 0) return `${mins}m`; return "now"; }; const getNotificationTitle = (n: Notification) => { const count = n.count || 1; switch (n.type) { case "follows": { const who = n.follows?.[0]?.user?.display_name || "Someone"; const others = count > 1 ? ` +${count - 1}` : ""; return `${who}${others} followed you`; } case "likes": { const who = n.reactions?.[0]?.user?.display_name || "Someone"; const others = count > 1 ? ` +${count - 1}` : ""; return `${who}${others} liked`; } case "recasts": { const who = n.reactions?.[0]?.user?.display_name || "Someone"; const others = count > 1 ? ` +${count - 1}` : ""; return `${who}${others} recasted`; } case "mention": return `${n.cast?.author?.display_name || "Someone"} mentioned you`; case "reply": return `${n.cast?.author?.display_name || "Someone"} replied`; default: return n.type; } }; if (!isReady) { return (

Loading...

); } return (

jack-template

{/* User card */} {context?.user && (
{context.user.displayName}

{context.user.displayName}

@{context.user.username}

)} {/* Tabs */}
{/* Guestbook Tab */} {activeTab === "guestbook" && (
{/* Add entry form */}
setNewMessage(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleAddGuestbookEntry()} placeholder="Sign the guestbook..." maxLength={140} className="flex-1 px-3 py-2 bg-zinc-900 rounded-lg text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:ring-1 focus:ring-violet-500" />
{/* Error */} {addEntry.isError && addEntry.error && (

{addEntry.error.message}

)} {/* Entries */} {guestbookLoading &&

Loading...

} {guestbookError &&

Failed to load

}
{guestbookEntries?.map((entry: GuestbookEntry) => (
{entry.pfp_url ? ( ) : (
)}

{entry.display_name || `@${entry.username}`}

{formatTime(entry.created_at)}

{entry.message}

))} {!guestbookLoading && !guestbookError && guestbookEntries?.length === 0 && (

Be the first to sign the guestbook!

)}
)} {/* Notifications Tab */} {activeTab === "notifications" && (
{notifLoading &&

Loading...

} {notifError &&

{notifError}

}
{notifications.map((n, i) => (

{getNotificationTitle(n)}

{formatTime(n.most_recent_timestamp)}
{n.cast?.text && (

{n.cast.text}

)}
))}
{!notifLoading && !notifError && notifications.length === 0 && (

No notifications

)}
)} {/* AI Tab */} {activeTab === "ai" && (
{/* Profile Analysis - Dedicated Endpoint Pattern */}

What kind of Farcaster user are you?

Let AI analyze your profile and categorize your Farcaster activity

{!context?.user && (

User context not available

)} {context?.user && !aiResult && !aiLoading && ( )} {aiLoading && (
Analyzing...
)} {aiError && (

{aiError}

)} {aiResult && (
{aiResult.category === "builder" && "🛠️"} {aiResult.category === "creator" && "🎨"} {aiResult.category === "collector" && "💎"} {aiResult.category === "connector" && "🤝"} {aiResult.category === "lurker" && "👀"}

{aiResult.category}

{aiResult.reason}

)}
{/* Divider */}
{/* Cast Idea Generator - useAI Hook Pattern */}

Need a cast idea?

Generate creative cast ideas with AI

{!castIdea && !castIdeaLoading && ( )} {castIdeaLoading && (
Generating...
)} {castIdeaError && (

{castIdeaError}

)} {castIdea && (

{castIdea}

Powered by useAI() hook

)}
)} {/* Close button */}
{/* Share Sheet */} setShowShareSheet(false)} text={`I just signed the jack-template guestbook! "${lastSignedMessage}"`} embedUrl={getShareEmbedUrl()} title="Share your signature" user={ context?.user ? { username: context.user.username, displayName: context.user.displayName, pfpUrl: context.user.pfpUrl, } : undefined } />
); }