Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Technical DocumentationTools ReferenceComponent Library Reference

Component Library Reference

🎨 Universal Component System

MyStoryFlow’s component library provides reusable, standardized components that ensure consistent UI/UX across all tools while minimizing development time and maintenance overhead.

📋 Component Architecture Overview

Component Hierarchy

Universal Components ├── Core Tool Components (per tool) │ ├── \{Tool\}Landing.tsx # Main generator interface │ ├── \{Tool\}Browse.tsx # Content discovery │ ├── \{Tool\}Card.tsx # Content display cards │ ├── \{Tool\}Writer.tsx # Response creation │ └── \{Tool\}PublicView.tsx # SEO-optimized sharing ├── Shared Components (cross-tool) │ ├── UniversalWriter.tsx # Universal response editor │ ├── ShareManager.tsx # Social sharing system │ ├── ExportEngine.tsx # Multi-format exports │ ├── FilterPanel.tsx # Advanced filtering │ ├── AnalysisPanel.tsx # Content analysis display │ └── ConversionFooter.tsx # CTA and conversion └── UI Foundation (@mystoryflow/ui) ├── Button, Input, Textarea ├── Card, Badge, Select ├── Dialog, Sheet, Tabs └── Form, Toast, Progress

🏗️ Core Tool Components

{Tool}Landing Component

Main generator interface that handles content generation and display.

// /components/\{tool\}/\{Tool\}Landing.tsx import { ToolLandingProps, ToolOptions, ToolResult } from './types' interface ToolLandingProps { params?: { [key: string]: string } searchParams?: { [key: string]: string | string[] | undefined } } export function ToolLanding({ params, searchParams }: ToolLandingProps) { // State management const [options, setOptions] = useState<ToolOptions>({}) const [result, setResult] = useState<ToolResult | null>(null) const [isGenerating, setIsGenerating] = useState(false) const [sessionId, setSessionId] = useState('') // Core functionality const handleGenerate = async () => { /* Generation logic */ } const updateOption = (key, value) => { /* Option updates */ } const isFormValid = () => { /* Validation */ } return ( <div className="max-w-4xl mx-auto"> {/* Hero Section */} <HeroSection config={toolConfig} /> {/* Options Form */} <OptionsForm options={options} onChange={updateOption} config={toolConfig} /> {/* Generate Button */} <GenerateButton onClick={handleGenerate} disabled={isGenerating || !isFormValid()} loading={isGenerating} /> {/* Results Display */} {result && ( <ResultsDisplay result={result} toolName={toolConfig.name} /> )} {/* Conversion CTA */} <ConversionCTA show={shouldShowCTA} /> {/* Featured Content */} <FeaturedContent toolName={toolConfig.slug} /> </div> ) }

Props Interface

interface ToolLandingProps { params?: Record<string, string> searchParams?: Record<string, string | string[] | undefined> } interface ToolConfig { name: string // "Plot Twist Generator" slug: string // "plot-twists" description: string // Tool description outputName: string // "Plot Twists" category: string // "Story Development" fields: FormField[] // Configuration fields features: string[] // Key features list examples: string[] // Usage examples } interface FormField { key: string // Field identifier label: string // Display label type: 'input' | 'select' | 'textarea' | 'checkbox' placeholder?: string description?: string required?: boolean options?: Array<{ // For select fields value: string label: string description?: string }> validation?: { minLength?: number maxLength?: number pattern?: RegExp } }

{Tool}Browse Component

Content discovery interface with filtering and search.

// /components/\{tool\}/\{Tool\}Browse.tsx export function ToolBrowse() { // State for filters and results const [results, setResults] = useState<BrowseResult[]>([]) const [loading, setLoading] = useState(false) const [filters, setFilters] = useState<BrowseFilters>({}) // Data fetching and filtering const fetchResults = async () => { /* Fetch logic */ } const updateFilter = (key, value) => { /* Filter updates */ } return ( <div className="max-w-6xl mx-auto"> {/* Header */} <BrowseHeader toolName={toolConfig.name} description={`Discover ${toolConfig.outputName.toLowerCase()} created by our community`} /> {/* Filter Bar */} <FilterBar filters={filters} onFilterChange={updateFilter} searchPlaceholder={`Search ${toolConfig.outputName.toLowerCase()}...`} /> {/* Content Grid */} <ContentGrid results={results} loading={loading} onLoadMore={fetchResults} renderCard={(item) => ( <ToolCard item={item} toolName={toolConfig.slug} onView={handleView} onShare={handleShare} /> )} /> </div> ) }

Props Interface

interface BrowseFilters { search: string sort: 'newest' | 'popular' | 'featured' | 'rating' public: boolean featured: boolean page: number category?: string tags?: string[] } 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 isPublic: boolean isFeatured: boolean }

{Tool}Card Component

Reusable content display card with consistent styling.

// /components/\{tool\}/\{Tool\}Card.tsx interface ToolCardProps { item: BrowseResult toolName: string onView: (item: BrowseResult) => void onShare: (item: BrowseResult) => void compact?: boolean showAnalytics?: boolean } export function ToolCard({ item, toolName, onView, onShare, compact = false, showAnalytics = true }: ToolCardProps) { if (compact) { return ( <Card className="p-4 hover:shadow-md transition-shadow cursor-pointer" onClick={() => onView(item)}> {/* Compact layout */} <CompactCardContent item={item} /> </Card> ) } return ( <Card className="p-6 hover:shadow-lg transition-all"> {/* Header with metadata */} <CardHeader title={item.title} subtitle={item.seoDescription} metadata={{ featured: item.isFeatured, createdAt: item.createdAt, rating: item.averageRating }} /> {/* Keywords/Tags */} <KeywordTags keywords={item.keywords} maxShow={5} /> {/* Content Preview */} <ContentPreview content={item.content} toolName={toolName} /> {/* Footer with analytics and actions */} <CardFooter analytics={{ views: item.viewCount, shares: item.shareCount, responses: item.responseCount }} actions={[ { label: 'Share', icon: Share2, onClick: () => onShare(item), variant: 'outline' }, { label: 'View Full', onClick: () => onView(item), variant: 'default' } ]} showAnalytics={showAnalytics} /> </Card> ) }

{Tool}Writer Component

Response creation interface with auto-save functionality.

// /components/\{tool\}/\{Tool\}Writer.tsx interface ToolWriterProps { parentId: string toolName: string sessionId: string initialData?: ResponseData onResponseCreated?: (response: ResponseData) => void onResponseUpdated?: (response: ResponseData) => void } export function ToolWriter({ parentId, toolName, sessionId, initialData, onResponseCreated, onResponseUpdated }: ToolWriterProps) { // Use the universal writer with tool-specific configuration return ( <UniversalWriter parentId={parentId} toolName={toolName} sessionId={sessionId} initialData={initialData} onResponseCreated={onResponseCreated} onResponseUpdated={onResponseUpdated} config={{ placeholder: `Share your thoughts about this ${toolName.replace('-', ' ')}...`, helpText: `Write your creative response, story continuation, or insights inspired by this ${toolName.replace('-', ' ')}.`, features: ['auto-save', 'export', 'sharing', 'public-private-toggle'] }} /> ) }

{Tool}PublicView Component

SEO-optimized public sharing page.

// /components/\{tool\}/\{Tool\}PublicView.tsx interface ToolPublicViewProps { shareCode: string content: BrowseResult responses: ResponseData[] toolConfig: ToolConfig } export function ToolPublicView({ shareCode, content, responses, toolConfig }: ToolPublicViewProps) { return ( <div className="min-h-screen bg-background"> {/* SEO Structured Data */} <StructuredDataScript data={generateToolStructuredData(toolConfig, content)} /> <div className="container mx-auto px-4 py-8 max-w-4xl"> {/* Header with social proof */} <PublicViewHeader title={content.title} metadata={{ createdAt: content.createdAt, views: content.viewCount, shares: content.shareCount, category: toolConfig.category }} keywords={content.keywords} /> {/* Main Content Display */} <ContentDisplayCard content={content.content} toolName={toolConfig.slug} isPublicView={true} /> {/* Community Responses */} {responses.length > 0 && ( <CommunityResponsesSection responses={responses.filter(r => r.isPublic)} maxShow={10} /> )} {/* Call to Action */} <PublicViewCTA toolName={toolConfig.name} toolSlug={toolConfig.slug} description={`Create your own ${toolConfig.outputName.toLowerCase()} with AI`} /> {/* Related Tools */} <RelatedToolsSection currentTool={toolConfig.slug} maxShow={3} /> </div> </div> ) }

🔧 Shared Components

UniversalWriter Component

Standardized response editor used across all tools.

// /components/shared/UniversalWriter.tsx interface UniversalWriterProps { parentId: string toolName: string sessionId: string initialData?: ResponseData onResponseCreated?: (response: ResponseData) => void onResponseUpdated?: (response: ResponseData) => void config?: { placeholder?: string helpText?: string features?: Array<'auto-save' | 'export' | 'sharing' | 'public-private-toggle'> maxLength?: number minLength?: number } } export function UniversalWriter(props: UniversalWriterProps) { // State management for writing interface const [state, setState] = useState<WriterState>({ title: props.initialData?.title || '', content: props.initialData?.content || '', isPublic: props.initialData?.isPublic || false, // ... other state }) // Auto-save functionality const saveContent = useCallback(async (showToast = false, isAutoSave = false) => { // Save implementation with error handling }, [/* dependencies */]) // Keyboard shortcuts (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]) return ( <Card className="p-6"> {/* Writer Header */} <WriterHeader title={props.initialData?.id ? 'Edit Response' : 'Write Response'} status={{ lastSaved: state.lastSaved, hasUnsavedChanges: state.hasUnsavedChanges, isSaving: state.isSaving, saveStatus: state.saveStatus }} autoSave={{ enabled: state.autoSaveEnabled, onToggle: () => setState(prev => ({ ...prev, autoSaveEnabled: !prev.autoSaveEnabled })) }} /> {/* Title Input */} <Input placeholder={props.config?.placeholder || "Give your response a title..."} value={state.title} onChange={(e) => updateField('title', e.target.value)} className="text-lg font-medium mb-4" size="sm" /> {/* Content Editor */} <Textarea placeholder={props.config?.helpText || "Share your thoughts and creative response..."} value={state.content} onChange={(e) => updateField('content', e.target.value)} rows={12} className="resize-none mb-2" /> {/* Content Metrics */} <ContentMetrics wordCount={state.wordCount} characterCount={state.characterCount} showHelpText={true} /> {/* Action Bar */} <ActionBar visibility={{ isPublic: state.isPublic, onToggle: handlePublicToggle, showToggle: props.config?.features?.includes('public-private-toggle') ?? true }} actions={{ save: { onClick: () => saveContent(true), disabled: !canSave(), loading: state.isSaving }, share: props.config?.features?.includes('sharing') ? { onClick: handleShare, disabled: !state.lastSaved || state.hasUnsavedChanges, show: !!state.lastSaved } : undefined, export: props.config?.features?.includes('export') ? { onClick: handleExport, disabled: !state.lastSaved || state.hasUnsavedChanges, show: !!state.lastSaved } : undefined }} /> {/* Help Text */} <WriterHelpText features={props.config?.features || []} /> </Card> ) }

ShareManager Component

Universal sharing system for all content types.

// /components/shared/ShareManager.tsx interface ShareManagerProps { content: { id: string shareCode?: string title: string type: string // tool name } onShare?: (platform: string) => void } export function ShareManager({ content, onShare }: ShareManagerProps) { const [isOpen, setIsOpen] = useState(false) const [copied, setCopied] = useState(false) const shareUrl = `${window.location.origin}/${content.type}/share/${content.shareCode}` const shareOptions = [ { name: 'Copy Link', icon: Link, action: () => copyToClipboard(shareUrl), primary: true }, { name: 'Twitter', icon: Twitter, action: () => shareOnTwitter(content.title, shareUrl), color: '#1DA1F2' }, { name: 'Facebook', icon: Facebook, action: () => shareOnFacebook(shareUrl), color: '#1877F2' }, { name: 'LinkedIn', icon: Linkedin, action: () => shareOnLinkedIn(content.title, shareUrl), color: '#0A66C2' }, { name: 'Reddit', icon: MessageSquare, action: () => shareOnReddit(content.title, shareUrl), color: '#FF4500' }, { name: 'WhatsApp', icon: MessageCircle, action: () => shareOnWhatsApp(content.title, shareUrl), color: '#25D366' } ] return ( <Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogTrigger asChild> <Button variant="outline" size="sm"> <Share2 className="h-4 w-4 mr-1" /> Share </Button> </DialogTrigger> <DialogContent className="sm:max-w-md"> <DialogHeader> <DialogTitle>Share "{content.title}"</DialogTitle> <DialogDescription> Choose how you'd like to share this content </DialogDescription> </DialogHeader> <div className="space-y-4"> {/* Direct Link */} <div className="flex items-center space-x-2"> <Input value={shareUrl} readOnly className="flex-1" /> <Button onClick={() => copyToClipboard(shareUrl)} variant={copied ? "default" : "outline"} > {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} </Button> </div> {/* Social Platforms */} <div className="grid grid-cols-3 gap-2"> {shareOptions.slice(1).map((option) => ( <Button key={option.name} variant="outline" className="flex-col h-20 p-2" onClick={() => { option.action() onShare?.(option.name) }} > <option.icon className="h-6 w-6 mb-1" style={{ color: option.color }} /> <span className="text-xs">{option.name}</span> </Button> ))} </div> {/* Embed Code */} <details> <summary className="cursor-pointer text-sm font-medium">Embed Code</summary> <Textarea value={generateEmbedCode(shareUrl, content.title)} readOnly rows={3} className="mt-2 text-xs" /> </details> </div> </DialogContent> </Dialog> ) }

ExportEngine Component

Multi-format export system.

// /components/shared/ExportEngine.tsx interface ExportEngineProps { contentId: string toolName: string title: string formats?: Array<'json' | 'csv' | 'pdf' | 'html' | 'docx'> onExport?: (format: string) => void } export function ExportEngine({ contentId, toolName, title, formats = ['json', 'csv', 'pdf', 'html', 'docx'], onExport }: ExportEngineProps) { const [isOpen, setIsOpen] = useState(false) const [isExporting, setIsExporting] = useState<string | null>(null) const exportFormats = [ { format: 'json', name: 'JSON', description: 'Raw data format for developers', icon: Code, color: 'text-blue-600' }, { format: 'csv', name: 'CSV', description: 'Spreadsheet compatible format', icon: FileSpreadsheet, color: 'text-green-600' }, { format: 'pdf', name: 'PDF', description: 'Print-ready document format', icon: FileText, color: 'text-red-600' }, { format: 'html', name: 'HTML', description: 'Web page format', icon: Globe, color: 'text-orange-600' }, { format: 'docx', name: 'Word Document', description: 'Microsoft Word compatible', icon: FileText, color: 'text-blue-700' } ].filter(f => formats.includes(f.format as any)) const handleExport = async (format: string) => { setIsExporting(format) try { const response = await fetch(`/api/${toolName}/${contentId}/export`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ format }) }) if (response.ok) { // Trigger file download const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.${format}` document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) onExport?.(format) } else { throw new Error('Export failed') } } catch (error) { toast({ title: 'Export Failed', description: `Failed to export as ${format.toUpperCase()}. Please try again.`, variant: 'destructive' }) } finally { setIsExporting(null) } } return ( <Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogTrigger asChild> <Button variant="outline" size="sm"> <Download className="h-4 w-4 mr-1" /> Export </Button> </DialogTrigger> <DialogContent className="sm:max-w-md"> <DialogHeader> <DialogTitle>Export "{title}"</DialogTitle> <DialogDescription> Choose your preferred export format </DialogDescription> </DialogHeader> <div className="space-y-2"> {exportFormats.map((format) => ( <Button key={format.format} variant="ghost" className="w-full justify-start h-auto p-4" onClick={() => handleExport(format.format)} disabled={!!isExporting} > <div className="flex items-center space-x-3"> {isExporting === format.format ? ( <Loader2 className="h-5 w-5 animate-spin" /> ) : ( <format.icon className={`h-5 w-5 ${format.color}`} /> )} <div className="text-left"> <div className="font-medium">{format.name}</div> <div className="text-sm text-muted-foreground"> {format.description} </div> </div> </div> </Button> ))} </div> <div className="text-xs text-muted-foreground"> All exports include content metadata and analytics (when applicable) </div> </DialogContent> </Dialog> ) }

FilterPanel Component

Advanced filtering interface for browse pages.

// /components/shared/FilterPanel.tsx interface FilterPanelProps { filters: BrowseFilters onChange: (key: keyof BrowseFilters, value: any) => void options: { sortOptions?: Array<{ value: string; label: string }> categories?: Array<{ value: string; label: string; count?: number }> tags?: Array<{ value: string; label: string; count?: number }> } searchPlaceholder?: string } export function FilterPanel({ filters, onChange, options, searchPlaceholder = "Search content..." }: FilterPanelProps) { return ( <div className="space-y-4 mb-6"> {/* Search and Sort Row */} <div className="flex flex-col md:flex-row gap-4"> {/* Search Input */} <div className="flex-1 relative"> <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> <Input placeholder={searchPlaceholder} value={filters.search} onChange={(e) => onChange('search', e.target.value)} className="pl-10" size="sm" /> </div> {/* Sort Dropdown */} <Select value={filters.sort} onValueChange={(value) => onChange('sort', value)} > <SelectTrigger className="w-full md:w-40" size="sm"> <SelectValue /> </SelectTrigger> <SelectContent> {(options.sortOptions || defaultSortOptions).map((option) => ( <SelectItem key={option.value} value={option.value}> {option.label} </SelectItem> ))} </SelectContent> </Select> </div> {/* Filter Toggles */} <div className="flex flex-wrap gap-2"> <Button variant={filters.public ? "default" : "outline"} size="sm" onClick={() => onChange('public', !filters.public)} > <Eye className="h-4 w-4 mr-1" /> Public Only </Button> <Button variant={filters.featured ? "default" : "outline"} size="sm" onClick={() => onChange('featured', !filters.featured)} > <Sparkles className="h-4 w-4 mr-1" /> Featured </Button> {/* Advanced Filters Trigger */} <Sheet> <SheetTrigger asChild> <Button variant="outline" size="sm"> <Filter className="h-4 w-4 mr-1" /> More Filters </Button> </SheetTrigger> <SheetContent> <SheetHeader> <SheetTitle>Advanced Filters</SheetTitle> </SheetHeader> <div className="space-y-6 mt-6"> {/* Categories */} {options.categories && ( <div> <h3 className="font-medium mb-3">Categories</h3> <div className="space-y-2"> {options.categories.map((category) => ( <label key={category.value} className="flex items-center space-x-2"> <Checkbox checked={filters.category === category.value} onCheckedChange={(checked) => onChange('category', checked ? category.value : '') } /> <span className="flex-1">{category.label}</span> {category.count && ( <Badge variant="secondary" className="text-xs"> {category.count} </Badge> )} </label> ))} </div> </div> )} {/* Tags */} {options.tags && ( <div> <h3 className="font-medium mb-3">Tags</h3> <div className="flex flex-wrap gap-1"> {options.tags.map((tag) => ( <Badge key={tag.value} variant={filters.tags?.includes(tag.value) ? "default" : "outline"} className="cursor-pointer" onClick={() => { const currentTags = filters.tags || [] const newTags = currentTags.includes(tag.value) ? currentTags.filter(t => t !== tag.value) : [...currentTags, tag.value] onChange('tags', newTags) }} > {tag.label} {tag.count && ` (${tag.count})`} </Badge> ))} </div> </div> )} </div> </SheetContent> </Sheet> </div> </div> ) } const defaultSortOptions = [ { value: 'newest', label: 'Newest First' }, { value: 'popular', label: 'Most Popular' }, { value: 'featured', label: 'Featured' }, { value: 'rating', label: 'Highest Rated' } ]

AnalysisPanel Component

Displays AI-generated content analysis and insights.

// /components/shared/AnalysisPanel.tsx interface AnalysisPanelProps { analysis: { summary: string strengths: string[] suggestions: string[] quality: { score: number factors: Array<{ name: string score: number description: string }> } metadata?: { wordCount?: number readingTime?: number complexity?: 'beginner' | 'intermediate' | 'advanced' genre?: string } } toolName: string compact?: boolean } export function AnalysisPanel({ analysis, toolName, compact = false }: AnalysisPanelProps) { if (compact) { return ( <Card className="p-4 bg-muted/50"> <div className="flex items-start gap-3"> <div className="p-2 bg-primary/10 rounded-lg"> <BarChart3 className="h-4 w-4 text-primary" /> </div> <div className="flex-1 min-w-0"> <h4 className="font-medium text-sm mb-1">AI Analysis</h4> <p className="text-xs text-muted-foreground line-clamp-2"> {analysis.summary} </p> </div> </div> </Card> ) } return ( <Card className="p-6"> <div className="flex items-center gap-2 mb-4"> <div className="p-2 bg-primary/10 rounded-lg"> <BarChart3 className="h-5 w-5 text-primary" /> </div> <h3 className="font-semibold">AI Analysis & Insights</h3> </div> <div className="space-y-6"> {/* Summary */} <div> <h4 className="font-medium mb-2">Summary</h4> <p className="text-muted-foreground text-sm leading-relaxed"> {analysis.summary} </p> </div> {/* Quality Score */} <div> <div className="flex items-center justify-between mb-3"> <h4 className="font-medium">Quality Score</h4> <Badge variant="secondary" className="text-sm"> {Math.round(analysis.quality.score * 100)}/100 </Badge> </div> <div className="space-y-2"> {analysis.quality.factors.map((factor, index) => ( <div key={index} className="flex items-center gap-3"> <span className="text-sm min-w-0 flex-1">{factor.name}</span> <div className="flex-1 bg-muted rounded-full h-2"> <div className="bg-primary rounded-full h-2 transition-all" style={{ width: `${factor.score * 100}%` }} /> </div> <span className="text-xs text-muted-foreground min-w-0"> {Math.round(factor.score * 100)}% </span> </div> ))} </div> </div> {/* Strengths */} <div> <h4 className="font-medium mb-2 flex items-center gap-1"> <CheckCircle className="h-4 w-4 text-green-500" /> Strengths </h4> <ul className="space-y-1"> {analysis.strengths.map((strength, index) => ( <li key={index} className="text-sm text-muted-foreground flex items-start gap-2"> <span className="text-green-500 mt-1"></span> <span>{strength}</span> </li> ))} </ul> </div> {/* Suggestions */} <div> <h4 className="font-medium mb-2 flex items-center gap-1"> <Lightbulb className="h-4 w-4 text-yellow-500" /> Suggestions </h4> <ul className="space-y-1"> {analysis.suggestions.map((suggestion, index) => ( <li key={index} className="text-sm text-muted-foreground flex items-start gap-2"> <span className="text-yellow-500 mt-1"></span> <span>{suggestion}</span> </li> ))} </ul> </div> {/* Metadata */} {analysis.metadata && ( <div className="pt-4 border-t"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"> {analysis.metadata.wordCount && ( <div> <div className="text-2xl font-bold text-primary"> {analysis.metadata.wordCount} </div> <div className="text-xs text-muted-foreground">Words</div> </div> )} {analysis.metadata.readingTime && ( <div> <div className="text-2xl font-bold text-primary"> {analysis.metadata.readingTime} </div> <div className="text-xs text-muted-foreground">Min Read</div> </div> )} {analysis.metadata.complexity && ( <div> <div className="text-2xl font-bold text-primary capitalize"> {analysis.metadata.complexity} </div> <div className="text-xs text-muted-foreground">Level</div> </div> )} {analysis.metadata.genre && ( <div> <div className="text-2xl font-bold text-primary"> {analysis.metadata.genre} </div> <div className="text-xs text-muted-foreground">Genre</div> </div> )} </div> </div> )} </div> </Card> ) }

ConversionFooter Component

CTA and conversion optimization footer.

// /components/shared/ConversionFooter.tsx interface ConversionFooterProps { toolName?: string showOnCondition?: 'always' | 'rate-limited' | 'after-generation' | 'never' customCTA?: { headline: string description: string buttonText: string buttonUrl: string } } export function ConversionFooter({ toolName, showOnCondition = 'after-generation', customCTA }: ConversionFooterProps) { const [show, setShow] = useState(showOnCondition === 'always') // Logic to determine when to show CTA based on condition useEffect(() => { if (showOnCondition === 'never') { setShow(false) } else if (showOnCondition === 'always') { setShow(true) } // Add other conditions as needed }, [showOnCondition]) if (!show) return null const cta = customCTA || { headline: `Love using ${toolName || 'our tools'}?`, description: 'Join MyStoryFlow for unlimited generations, saved content, and premium features.', buttonText: 'Get Started Free', buttonUrl: 'https://mystoryflow.com/signup?source=tools' } return ( <Card className="p-6 border-primary bg-primary/5 mt-8"> <div className="text-center max-w-2xl mx-auto"> <h3 className="text-xl font-semibold mb-2">{cta.headline}</h3> <p className="text-muted-foreground mb-6">{cta.description}</p> <div className="flex flex-col sm:flex-row items-center justify-center gap-3"> <Button asChild size="lg"> <a href={cta.buttonUrl}> {cta.buttonText} </a> </Button> <Button variant="outline" size="lg" onClick={() => setShow(false)} > Maybe Later </Button> </div> {/* Social Proof */} <div className="flex items-center justify-center gap-4 mt-6 text-sm text-muted-foreground"> <span className="flex items-center gap-1"> <Users className="h-4 w-4" /> 10,000+ writers </span> <span className="flex items-center gap-1"> <Star className="h-4 w-4 text-yellow-500 fill-yellow-500" /> 4.9/5 rating </span> <span className="flex items-center gap-1"> <Shield className="h-4 w-4" /> Always free </span> </div> </div> </Card> ) }

🎨 Styling Guidelines

Design System Integration

All components use the @mystoryflow/ui design system:

  • Consistent sizing: size="sm" for tools-app
  • Color palette: Primary purple (#7c3aed), semantic colors
  • Typography: Inter font family, consistent sizing scale
  • Spacing: Tailwind spacing units (4px base)
  • Shadows: Subtle elevation system
  • Animations: Smooth transitions, loading states

Component Styling Standards

// Standard component styling patterns const componentStyles = { cards: "hover:shadow-lg transition-all duration-200", buttons: "transition-colors duration-150", inputs: "focus:ring-2 focus:ring-primary/20", loading: "animate-pulse", success: "border-green-200 bg-green-50 text-green-800", error: "border-red-200 bg-red-50 text-red-800", warning: "border-yellow-200 bg-yellow-50 text-yellow-800" }

🧩 Component Composition Patterns

Higher-Order Components

// withToolProvider - provides tool configuration export function withToolProvider<P extends object>( Component: React.ComponentType<P>, toolConfig: ToolConfig ) { return function ToolProviderWrapper(props: P) { return ( <ToolProvider config={toolConfig}> <Component {...props} /> </ToolProvider> ) } } // withSessionManagement - handles session state export function withSessionManagement<P extends object>( Component: React.ComponentType<P & { sessionId: string }> ) { return function SessionWrapper(props: P) { const { sessionId } = useSession() return <Component {...props} sessionId={sessionId} /> } }

Custom Hooks

// useToolGeneration - standardized generation logic export function useToolGeneration(toolName: string) { const [isGenerating, setIsGenerating] = useState(false) const [result, setResult] = useState(null) const [error, setError] = useState(null) const generate = useCallback(async (options: any) => { setIsGenerating(true) setError(null) try { const response = await fetch(`/api/${toolName}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: getSessionId(), options }) }) const data = await response.json() if (data.success) { setResult(data.data) } else { setError(data.error) } } catch (err) { setError({ message: 'Generation failed. Please try again.' }) } finally { setIsGenerating(false) } }, [toolName]) return { generate, isGenerating, result, error } } // useAutoSave - automatic content saving export function useAutoSave( saveFunction: () => Promise<any>, dependencies: any[], interval = 30000 ) { const timeoutRef = useRef<NodeJS.Timeout>() useEffect(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current) } timeoutRef.current = setTimeout(() => { saveFunction().catch(console.error) }, interval) return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current) } } }, dependencies) }

This comprehensive component library reference ensures consistent, maintainable, and reusable UI components across all MyStoryFlow tools while providing flexibility for tool-specific customization when needed.