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.