Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Technical DocumentationTools InfrastructureComponent Architecture & Reusable Patterns

Component Architecture & Reusable Patterns

🏗️ Universal Component System

MyStoryFlow’s component architecture maximizes code reuse and consistency across all tools. Every tool follows identical component patterns with 95% shared code and minimal tool-specific customization.

📋 Component Hierarchy Standard

Core Tool Components Structure

Every tool implements this exact component hierarchy:

src/components/\{tool\}/ ├── \{Tool\}Landing.tsx # Main tool page (generator + hero) ├── \{Tool\}Browse.tsx # Browse/discover existing content ├── \{Tool\}Writer.tsx # User response writing interface ├── \{Tool\}Card.tsx # Content display card ├── \{Tool\}PublicView.tsx # SEO-optimized shared content view └── shared/ # Tool-specific shared components ├── \{Tool\}FilterPanel.tsx ├── \{Tool\}AnalysisPanel.tsx └── \{Tool\}ExportOptions.tsx

Universal Page Structure

// Universal page component pattern export default async function ToolPage({ params, searchParams }: { params: Promise<{ [key: string]: string }> searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { const resolvedParams = await params const resolvedSearchParams = await searchParams return ( <div className="min-h-screen bg-background"> <ToolHeader /> <main className="container mx-auto px-4 py-8"> <ToolLanding params={resolvedParams} searchParams={resolvedSearchParams} /> </main> <ConversionFooter /> </div> ) }

🎨 Core Landing Component Pattern

{Tool}Landing.tsx - Master Template

'use client' import { useState, useEffect, useCallback } from 'react' import { Button } from '@mystoryflow/ui/button' import { Card } from '@mystoryflow/ui/card' import { Input } from '@mystoryflow/ui/input' import { Textarea } from '@mystoryflow/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@mystoryflow/ui/select' import { useToast } from '@mystoryflow/ui/use-toast' import { Loader2, Download, Share2, BookOpen, Sparkles } from 'lucide-react' // Universal interfaces interface ToolLandingProps { params?: { [key: string]: string } searchParams?: { [key: string]: string | string[] | undefined } } interface ToolOptions { // Tool-specific options interface // Each tool extends this base interface } interface ToolResult { id: string shareCode: string title: string content: any analysis: any seoMetadata: { title: string description: string keywords: string[] } aiMetadata: { model: string tokensUsed: number processingTime: number confidenceScore: number } } export function ToolLanding({ params, searchParams }: ToolLandingProps) { // =========================================== // STATE MANAGEMENT (UNIVERSAL PATTERN) // =========================================== const [options, setOptions] = useState<ToolOptions>({ // Tool-specific default options }) const [result, setResult] = useState<ToolResult | null>(null) const [isGenerating, setIsGenerating] = useState(false) const [sessionId, setSessionId] = useState<string>('') const [generationCount, setGenerationCount] = useState(0) const [showConversionCTA, setShowConversionCTA] = useState(false) const { toast } = useToast() // =========================================== // SESSION MANAGEMENT (UNIVERSAL) // =========================================== useEffect(() => { // Initialize or retrieve session ID let currentSessionId = localStorage.getItem('mystoryflow_session_id') if (!currentSessionId) { currentSessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` localStorage.setItem('mystoryflow_session_id', currentSessionId) } setSessionId(currentSessionId) // Load generation count for conversion tracking const savedCount = localStorage.getItem(`${tool}_generation_count`) setGenerationCount(parseInt(savedCount || '0')) }, []) // =========================================== // GENERATION HANDLER (UNIVERSAL PATTERN) // =========================================== const handleGenerate = useCallback(async () => { if (!sessionId) return setIsGenerating(true) setResult(null) try { const response = await fetch(`/api/{tool}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId }, body: JSON.stringify({ sessionId, options }) }) const data = await response.json() if (!response.ok) { // Handle rate limiting with conversion opportunity if (response.status === 429) { setShowConversionCTA(true) toast({ title: "Generation Limit Reached", description: data.error.message, variant: "destructive" }) return } throw new Error(data.error?.message || 'Generation failed') } setResult(data.data) setGenerationCount(prev => { const newCount = prev + 1 localStorage.setItem(`${tool}_generation_count`, newCount.toString()) return newCount }) // Show conversion CTA based on usage if (data.meta?.conversionOpportunity) { setShowConversionCTA(true) } toast({ title: "✨ Content Generated!", description: "Your content is ready. You can now write a response or share it." }) } catch (error) { console.error('Generation error:', error) toast({ title: "Generation Failed", description: error.message || "Please try again in a moment.", variant: "destructive" }) } finally { setIsGenerating(false) } }, [sessionId, options, toast]) // =========================================== // TOOL-SPECIFIC OPTION HANDLERS // =========================================== const updateOption = useCallback((key: keyof ToolOptions, value: any) => { setOptions(prev => ({ ...prev, [key]: value })) }, []) // =========================================== // RENDER: HERO SECTION (UNIVERSAL) // =========================================== const renderHeroSection = () => ( <div className="text-center mb-12"> <div className="flex items-center justify-center gap-2 mb-4"> <Sparkles className="h-8 w-8 text-primary" /> <h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-primary via-purple-500 to-pink-500 bg-clip-text text-transparent"> {toolConfig.name} </h1> </div> <p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-8"> {toolConfig.description} </p> <div className="flex items-center justify-center gap-4 text-sm text-muted-foreground"> <span className="flex items-center gap-1"> <BookOpen className="h-4 w-4" /> Free to use </span> <span className="flex items-center gap-1"> <Share2 className="h-4 w-4" /> Easy sharing </span> <span className="flex items-center gap-1"> <Download className="h-4 w-4" /> Multiple formats </span> </div> </div> ) // =========================================== // RENDER: OPTIONS FORM (TOOL-SPECIFIC) // =========================================== const renderOptionsForm = () => ( <Card className="p-6 mb-8"> <h2 className="text-xl font-semibold mb-4">Customize Your {toolConfig.outputName}</h2> <div className="grid gap-4"> {/* Tool-specific form fields */} {toolConfig.fields.map((field) => ( <div key={field.key} className="space-y-2"> <label className="text-sm font-medium">{field.label}</label> {field.type === 'select' && ( <Select value={options[field.key] || ''} onValueChange={(value) => updateOption(field.key, value)} > <SelectTrigger size="sm"> <SelectValue placeholder={field.placeholder} /> </SelectTrigger> <SelectContent> {field.options.map((option) => ( <SelectItem key={option.value} value={option.value}> {option.label} </SelectItem> ))} </SelectContent> </Select> )} {field.type === 'input' && ( <Input size="sm" placeholder={field.placeholder} value={options[field.key] || ''} onChange={(e) => updateOption(field.key, e.target.value)} /> )} {field.type === 'textarea' && ( <Textarea placeholder={field.placeholder} value={options[field.key] || ''} onChange={(e) => updateOption(field.key, e.target.value)} rows={3} /> )} {field.description && ( <p className="text-xs text-muted-foreground">{field.description}</p> )} </div> ))} </div> </Card> ) // =========================================== // RENDER: GENERATION BUTTON (UNIVERSAL) // =========================================== const renderGenerateButton = () => ( <div className="text-center mb-8"> <Button size="lg" onClick={handleGenerate} disabled={isGenerating || !isFormValid()} className="px-8 py-3 text-lg font-semibold" > {isGenerating ? ( <> <Loader2 className="mr-2 h-5 w-5 animate-spin" /> Generating... </> ) : ( <> <Sparkles className="mr-2 h-5 w-5" /> Generate {toolConfig.outputName} </> )} </Button> {generationCount > 0 && ( <p className="text-sm text-muted-foreground mt-2"> You've generated {generationCount} {toolConfig.outputName.toLowerCase()}{generationCount !== 1 ? 's' : ''} today </p> )} </div> ) // =========================================== // RENDER: RESULTS SECTION (UNIVERSAL PATTERN) // =========================================== const renderResults = () => { if (!result) return null return ( <div className="space-y-6"> <Card className="p-6"> <div className="flex items-center justify-between mb-4"> <h2 className="text-2xl font-bold">{result.title}</h2> <div className="flex items-center gap-2"> <ShareButton result={result} /> <ExportButton result={result} /> </div> </div> {/* Tool-specific content display */} <ToolContentDisplay content={result.content} /> {/* Analysis panel if available */} {result.analysis && ( <ToolAnalysisPanel analysis={result.analysis} /> )} {/* AI metadata */} <div className="mt-6 p-4 bg-muted rounded-lg"> <div className="flex items-center justify-between text-sm text-muted-foreground"> <span>Generated with {result.aiMetadata.model}</span> <span>{result.aiMetadata.processingTime}ms • {result.aiMetadata.tokensUsed} tokens</span> </div> </div> </Card> {/* Writer component for user responses */} <ToolWriter parentId={result.id} sessionId={sessionId} onResponseCreated={handleResponseCreated} /> </div> ) } // =========================================== // RENDER: CONVERSION CTA (UNIVERSAL) // =========================================== const renderConversionCTA = () => { if (!showConversionCTA) return null return ( <Card className="p-6 border-primary bg-primary/5"> <div className="text-center"> <h3 className="text-xl font-semibold mb-2">Love using our {toolConfig.name}?</h3> <p className="text-muted-foreground mb-4"> Join MyStoryFlow for unlimited generations, saved content, and premium features. </p> <div className="flex items-center justify-center gap-3"> <Button asChild> <a href="https://mystoryflow.com/signup?source=tools_{tool}"> Get Started Free </a> </Button> <Button variant="outline" onClick={() => setShowConversionCTA(false)}> Maybe Later </Button> </div> </div> </Card> ) } // =========================================== // MAIN RENDER // =========================================== return ( <div className="max-w-4xl mx-auto"> {renderHeroSection()} {renderOptionsForm()} {renderGenerateButton()} {renderResults()} {renderConversionCTA()} <FeaturedContent toolName="{tool}" /> </div> ) } // =========================================== // TOOL-SPECIFIC CONFIGURATION // =========================================== const toolConfig = { name: "Tool Name", description: "Tool description for SEO and user understanding", outputName: "Generated Content", fields: [ { key: "fieldName", label: "Field Label", type: "select" | "input" | "textarea", placeholder: "Placeholder text", description: "Optional field description", options: [{ value: "value", label: "Label" }] // For select fields } ] } // =========================================== // VALIDATION HELPER // =========================================== function isFormValid(): boolean { // Tool-specific validation logic return true }

🎯 Browse Component Pattern

{Tool}Browse.tsx - Discovery Interface

'use client' import { useState, useEffect, useCallback } from 'react' import { Button } from '@mystoryflow/ui/button' import { Card } from '@mystoryflow/ui/card' import { Input } from '@mystoryflow/ui/input' import { Badge } from '@mystoryflow/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@mystoryflow/ui/select' import { Search, Filter, Sparkles, Eye, Share2, MessageSquare } from 'lucide-react' interface BrowseFilters { search: string sort: 'newest' | 'popular' | 'featured' | 'rating' public: boolean featured: boolean page: number } interface BrowseResult { id: string shareCode: string title: string content: any seoTitle: string seoDescription: string keywords: string[] viewCount: number shareCount: number responseCount: number averageRating: number createdAt: string } export function ToolBrowse() { // =========================================== // STATE MANAGEMENT // =========================================== const [results, setResults] = useState<BrowseResult[]>([]) const [loading, setLoading] = useState(false) const [hasMore, setHasMore] = useState(false) const [filters, setFilters] = useState<BrowseFilters>({ search: '', sort: 'newest', public: true, featured: false, page: 1 }) // =========================================== // DATA FETCHING // =========================================== const fetchResults = useCallback(async (resetPage = false) => { setLoading(true) try { const currentPage = resetPage ? 1 : filters.page const searchParams = new URLSearchParams({ page: currentPage.toString(), limit: '20', sort: filters.sort, public: filters.public.toString(), featured: filters.featured.toString(), ...(filters.search && { search: filters.search }) }) const response = await fetch(`/api/{tool}?${searchParams}`) const data = await response.json() if (data.success) { if (resetPage) { setResults(data.data) } else { setResults(prev => [...prev, ...data.data]) } setHasMore(data.meta.pagination.hasNext) } } catch (error) { console.error('Browse fetch error:', error) } finally { setLoading(false) } }, [filters]) // =========================================== // FILTER HANDLERS // =========================================== const updateFilter = useCallback((key: keyof BrowseFilters, value: any) => { setFilters(prev => ({ ...prev, [key]: value, page: 1 })) }, []) // Debounced search useEffect(() => { const debounceTimer = setTimeout(() => { fetchResults(true) }, 300) return () => clearTimeout(debounceTimer) }, [filters.search, filters.sort, filters.public, filters.featured]) // =========================================== // RENDER: FILTER BAR // =========================================== const renderFilterBar = () => ( <div className="flex flex-col md:flex-row gap-4 mb-6"> <div className="flex-1 relative"> <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> <Input size="sm" placeholder="Search content..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-10" /> </div> <Select value={filters.sort} onValueChange={(value) => updateFilter('sort', value)}> <SelectTrigger size="sm" className="w-full md:w-40"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="newest">Newest First</SelectItem> <SelectItem value="popular">Most Popular</SelectItem> <SelectItem value="featured">Featured</SelectItem> <SelectItem value="rating">Highest Rated</SelectItem> </SelectContent> </Select> <div className="flex gap-2"> <Button variant={filters.featured ? "default" : "outline"} size="sm" onClick={() => updateFilter('featured', !filters.featured)} className="flex items-center gap-1" > <Sparkles className="h-4 w-4" /> Featured </Button> <Button variant="outline" size="sm" className="flex items-center gap-1" > <Filter className="h-4 w-4" /> More Filters </Button> </div> </div> ) // =========================================== // RENDER: CONTENT GRID // =========================================== const renderContentGrid = () => ( <div className="grid gap-4 md:gap-6"> {results.map((item) => ( <ToolCard key={item.id} item={item} onView={handleItemView} onShare={handleItemShare} /> ))} {hasMore && ( <div className="text-center"> <Button variant="outline" onClick={() => fetchResults()} disabled={loading} > {loading ? "Loading..." : "Load More"} </Button> </div> )} </div> ) return ( <div className="max-w-6xl mx-auto"> <div className="text-center mb-8"> <h1 className="text-3xl font-bold mb-2">Browse {toolConfig.name}</h1> <p className="text-muted-foreground"> Discover and get inspired by content created by our community </p> </div> {renderFilterBar()} {renderContentGrid()} </div> ) }

✍️ Writer Component Pattern

{Tool}Writer.tsx - Response Creation

'use client' import { useState, useEffect, useCallback } from 'react' import { Button } from '@mystoryflow/ui/button' import { Card } from '@mystoryflow/ui/card' import { Input } from '@mystoryflow/ui/input' import { Textarea } from '@mystoryflow/ui/textarea' import { Badge } from '@mystoryflow/ui/badge' import { useToast } from '@mystoryflow/ui/use-toast' import { Save, Share2, Download, Eye, EyeOff, Clock } from 'lucide-react' interface WriterProps { parentId: string sessionId: string onResponseCreated?: (response: any) => void } interface WriterState { title: string content: string isPublic: boolean lastSaved: Date | null wordCount: number isSaving: boolean autoSaveEnabled: boolean } export function ToolWriter({ parentId, sessionId, onResponseCreated }: WriterProps) { // =========================================== // STATE MANAGEMENT // =========================================== const [state, setState] = useState<WriterState>({ title: '', content: '', isPublic: false, lastSaved: null, wordCount: 0, isSaving: false, autoSaveEnabled: true }) const { toast } = useToast() // =========================================== // AUTO-SAVE FUNCTIONALITY // =========================================== const saveContent = useCallback(async (showToast = false) => { if (!state.title.trim() || !state.content.trim()) return setState(prev => ({ ...prev, isSaving: true })) try { const response = await fetch(`/api/{tool}/${parentId}/responses`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId }, body: JSON.stringify({ sessionId, title: state.title.trim(), content: state.content.trim(), isPublic: state.isPublic }) }) const data = await response.json() if (data.success) { setState(prev => ({ ...prev, lastSaved: new Date() })) if (showToast) { toast({ title: "Response Saved!", description: "Your response has been saved successfully." }) } onResponseCreated?.(data.data) } else { throw new Error(data.error?.message || 'Failed to save') } } catch (error) { console.error('Save error:', error) if (showToast) { toast({ title: "Save Failed", description: error.message, variant: "destructive" }) } } finally { setState(prev => ({ ...prev, isSaving: false })) } }, [state.title, state.content, state.isPublic, parentId, sessionId, toast, onResponseCreated]) // Auto-save every 30 seconds useEffect(() => { if (!state.autoSaveEnabled || !state.title.trim() || !state.content.trim()) return const autoSaveTimer = setTimeout(() => { saveContent(false) }, 30000) return () => clearTimeout(autoSaveTimer) }, [state.title, state.content, state.autoSaveEnabled, saveContent]) // Keyboard shortcut for manual save (Cmd/Ctrl + S) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault() saveContent(true) } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [saveContent]) // Update word count useEffect(() => { const words = state.content.trim().split(/\s+/).filter(word => word.length > 0) setState(prev => ({ ...prev, wordCount: words.length })) }, [state.content]) // =========================================== // EVENT HANDLERS // =========================================== const updateField = useCallback((field: keyof WriterState, value: any) => { setState(prev => ({ ...prev, [field]: value })) }, []) const handleManualSave = useCallback(() => { saveContent(true) }, [saveContent]) const handlePublicToggle = useCallback(() => { setState(prev => ({ ...prev, isPublic: !prev.isPublic })) }, []) // =========================================== // RENDER: WRITER INTERFACE // =========================================== return ( <Card className="p-6"> <div className="flex items-center justify-between mb-4"> <h3 className="text-lg font-semibold">Write Your Response</h3> <div className="flex items-center gap-2 text-sm text-muted-foreground"> {state.lastSaved && ( <span className="flex items-center gap-1"> <Clock className="h-3 w-3" /> Saved {formatTimeAgo(state.lastSaved)} </span> )} {state.isSaving && ( <Badge variant="secondary">Saving...</Badge> )} </div> </div> <div className="space-y-4"> {/* Title Input */} <div> <Input size="sm" placeholder="Give your response a title..." value={state.title} onChange={(e) => updateField('title', e.target.value)} className="text-lg font-medium" /> </div> {/* Content Textarea */} <div> <Textarea placeholder="Share your thoughts, story, or response here..." value={state.content} onChange={(e) => updateField('content', e.target.value)} rows={12} className="resize-none" /> <div className="flex items-center justify-between mt-2 text-sm text-muted-foreground"> <span>{state.wordCount} words</span> <span>Tip: Press Cmd/Ctrl + S to save manually</span> </div> </div> {/* Action Buttons */} <div className="flex items-center justify-between pt-4 border-t"> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" onClick={handlePublicToggle} className="flex items-center gap-1" > {state.isPublic ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />} {state.isPublic ? 'Public' : 'Private'} </Button> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" onClick={handleManualSave} disabled={state.isSaving || !state.title.trim() || !state.content.trim()} className="flex items-center gap-1" > <Save className="h-4 w-4" /> Save </Button> {state.lastSaved && ( <> <Button variant="outline" size="sm" className="flex items-center gap-1" > <Share2 className="h-4 w-4" /> Share </Button> <Button variant="outline" size="sm" className="flex items-center gap-1" > <Download className="h-4 w-4" /> Export </Button> </> )} </div> </div> </div> </Card> ) } // =========================================== // HELPER FUNCTIONS // =========================================== function formatTimeAgo(date: Date): string { const now = new Date() const diff = now.getTime() - date.getTime() const minutes = Math.floor(diff / 60000) if (minutes < 1) return 'just now' if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) return `${days}d ago` }

🎴 Card Component Pattern

{Tool}Card.tsx - Content Display

import { Card } from '@mystoryflow/ui/card' import { Button } from '@mystoryflow/ui/button' import { Badge } from '@mystoryflow/ui/badge' import { Eye, Share2, MessageSquare, Star, Calendar, Sparkles } from 'lucide-react' interface ToolCardProps { item: BrowseResult onView: (item: BrowseResult) => void onShare: (item: BrowseResult) => void compact?: boolean } export function ToolCard({ item, onView, onShare, compact = false }: ToolCardProps) { // =========================================== // RENDER: COMPACT CARD // =========================================== if (compact) { return ( <Card className="p-4 hover:shadow-md transition-shadow cursor-pointer" onClick={() => onView(item)}> <div className="flex items-start justify-between"> <div className="flex-1 min-w-0"> <h3 className="font-medium truncate">{item.title}</h3> <p className="text-sm text-muted-foreground line-clamp-2 mt-1"> {item.seoDescription} </p> </div> {item.averageRating > 0 && ( <div className="flex items-center gap-1 ml-3"> <Star className="h-3 w-3 text-yellow-500 fill-yellow-500" /> <span className="text-xs">{item.averageRating.toFixed(1)}</span> </div> )} </div> <div className="flex items-center justify-between mt-3 text-xs text-muted-foreground"> <span>{formatDate(item.createdAt)}</span> <div className="flex items-center gap-3"> <span className="flex items-center gap-1"> <Eye className="h-3 w-3" /> {item.viewCount} </span> <span className="flex items-center gap-1"> <MessageSquare className="h-3 w-3" /> {item.responseCount} </span> </div> </div> </Card> ) } // =========================================== // RENDER: FULL CARD // =========================================== return ( <Card className="p-6 hover:shadow-lg transition-all duration-200"> {/* Header */} <div className="flex items-start justify-between mb-4"> <div className="flex-1 min-w-0"> <div className="flex items-center gap-2 mb-2"> {item.featured && ( <Badge variant="secondary" className="flex items-center gap-1"> <Sparkles className="h-3 w-3" /> Featured </Badge> )} <span className="text-xs text-muted-foreground flex items-center gap-1"> <Calendar className="h-3 w-3" /> {formatDate(item.createdAt)} </span> </div> <h3 className="text-xl font-semibold mb-2 line-clamp-2">{item.title}</h3> <p className="text-muted-foreground line-clamp-3">{item.seoDescription}</p> </div> {item.averageRating > 0 && ( <div className="flex items-center gap-1 ml-4"> <Star className="h-4 w-4 text-yellow-500 fill-yellow-500" /> <span className="font-medium">{item.averageRating.toFixed(1)}</span> </div> )} </div> {/* Keywords */} {item.keywords.length > 0 && ( <div className="flex flex-wrap gap-1 mb-4"> {item.keywords.slice(0, 5).map((keyword) => ( <Badge key={keyword} variant="outline" className="text-xs"> {keyword} </Badge> ))} {item.keywords.length > 5 && ( <Badge variant="outline" className="text-xs"> +{item.keywords.length - 5} more </Badge> )} </div> )} {/* Tool-specific content preview */} <div className="mb-4 p-3 bg-muted rounded-lg"> <ToolContentPreview content={item.content} /> </div> {/* Footer */} <div className="flex items-center justify-between"> <div className="flex items-center gap-4 text-sm text-muted-foreground"> <span className="flex items-center gap-1"> <Eye className="h-4 w-4" /> {formatNumber(item.viewCount)} views </span> <span className="flex items-center gap-1"> <Share2 className="h-4 w-4" /> {formatNumber(item.shareCount)} shares </span> <span className="flex items-center gap-1"> <MessageSquare className="h-4 w-4" /> {formatNumber(item.responseCount)} responses </span> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" onClick={(e) => { e.stopPropagation() onShare(item) }}> <Share2 className="h-4 w-4 mr-1" /> Share </Button> <Button size="sm" onClick={() => onView(item)}> View Full </Button> </div> </div> </Card> ) } // =========================================== // HELPER FUNCTIONS // =========================================== function formatDate(dateString: string): string { const date = new Date(dateString) return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } function formatNumber(num: number): string { if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M` if (num >= 1000) return `${(num / 1000).toFixed(1)}K` return num.toString() }

🌐 Public View Component Pattern

{Tool}PublicView.tsx - SEO-Optimized Sharing

import { Metadata } from 'next' import { Card } from '@mystoryflow/ui/card' import { Button } from '@mystoryflow/ui/button' import { Badge } from '@mystoryflow/ui/badge' import { Eye, Share2, Calendar, Sparkles } from 'lucide-react' interface PublicViewProps { shareCode: string content: BrowseResult responses: any[] } // =========================================== // SEO METADATA GENERATION // =========================================== export async function generateMetadata({ params }: { params: Promise<{ shareCode: string }> }): Promise<Metadata> { const { shareCode } = await params // Fetch content for metadata const content = await fetchSharedContent(shareCode) if (!content) { return { title: 'Content Not Found | MyStoryFlow Tools', description: 'The shared content you are looking for could not be found.' } } const toolName = toolConfig.name const baseUrl = process.env.NEXT_PUBLIC_TOOLS_APP_URL || 'https://tools.mystoryflow.com' return { title: content.seoTitle || `${content.title} | ${toolName}`, description: content.seoDescription || `Check out this ${toolName.toLowerCase()} created with MyStoryFlow's AI-powered tools.`, keywords: content.keywords.join(', '), openGraph: { title: content.seoTitle || content.title, description: content.seoDescription || `AI-generated ${toolName.toLowerCase()} from MyStoryFlow`, type: 'article', url: `${baseUrl}/{tool}/share/${shareCode}`, images: [ { url: `${baseUrl}/api/og/{tool}/${shareCode}`, width: 1200, height: 630, alt: content.title } ] }, twitter: { card: 'summary_large_image', title: content.seoTitle || content.title, description: content.seoDescription, images: [`${baseUrl}/api/og/{tool}/${shareCode}`] }, alternates: { canonical: `${baseUrl}/{tool}/share/${shareCode}` }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1 } } } } // =========================================== // PUBLIC VIEW COMPONENT // =========================================== export function ToolPublicView({ shareCode, content, responses }: PublicViewProps) { return ( <div className="min-h-screen bg-background"> {/* Structured Data for SEO */} <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ '@context': 'https://schema.org', '@type': 'CreativeWork', name: content.title, description: content.seoDescription, author: { '@type': 'Organization', name: 'MyStoryFlow' }, publisher: { '@type': 'Organization', name: 'MyStoryFlow', url: 'https://mystoryflow.com' }, url: `https://tools.mystoryflow.com/{tool}/share/${shareCode}`, dateCreated: content.createdAt, genre: toolConfig.category, keywords: content.keywords.join(', ') }) }} /> <div className="container mx-auto px-4 py-8 max-w-4xl"> {/* Header */} <div className="text-center mb-8"> <div className="flex items-center justify-center gap-2 mb-4"> <Sparkles className="h-6 w-6 text-primary" /> <h1 className="text-3xl md:text-4xl font-bold">{content.title}</h1> </div> <div className="flex items-center justify-center gap-4 text-sm text-muted-foreground mb-6"> <span className="flex items-center gap-1"> <Calendar className="h-4 w-4" /> {formatDate(content.createdAt)} </span> <span className="flex items-center gap-1"> <Eye className="h-4 w-4" /> {formatNumber(content.viewCount)} views </span> <span className="flex items-center gap-1"> <Share2 className="h-4 w-4" /> {formatNumber(content.shareCount)} shares </span> </div> {content.keywords.length > 0 && ( <div className="flex flex-wrap justify-center gap-2 mb-6"> {content.keywords.map((keyword) => ( <Badge key={keyword} variant="outline"> {keyword} </Badge> ))} </div> )} </div> {/* Main Content */} <Card className="p-6 md:p-8 mb-8"> <ToolContentDisplay content={content.content} isPublicView={true} /> </Card> {/* Community Responses */} {responses.length > 0 && ( <div className="mb-8"> <h2 className="text-2xl font-bold mb-6">Community Responses</h2> <div className="space-y-4"> {responses.map((response) => ( <ResponseCard key={response.id} response={response} /> ))} </div> </div> )} {/* Call to Action */} <Card className="p-6 text-center bg-primary/5 border-primary"> <h3 className="text-xl font-semibold mb-2">Create Your Own {toolConfig.name}</h3> <p className="text-muted-foreground mb-4"> Use our AI-powered {toolConfig.name.toLowerCase()} to generate your own creative content. </p> <div className="flex items-center justify-center gap-3"> <Button asChild> <a href={`/{tool}`}> Try {toolConfig.name} </a> </Button> <Button variant="outline" asChild> <a href="https://mystoryflow.com"> Explore MyStoryFlow </a> </Button> </div> </Card> </div> </div> ) }

This comprehensive component architecture ensures every MyStoryFlow tool maintains consistency, performance, and user experience excellence while allowing for tool-specific customization where needed. The patterns are designed for maximum code reuse and minimal maintenance overhead.