import { useState, useEffect, useCallback, useRef } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { Search, SlidersHorizontal, ChevronDown, Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' import { PageMeta } from '@/components/common/PageMeta' import { BrandLogo } from '@/components/common/BrandLogo' import { FlowTemplateCard } from '@/components/public/FlowTemplateCard' import { ScriptTemplateCard } from '@/components/public/ScriptTemplateCard' import { TemplateDetailModal } from '@/components/public/TemplateDetailModal' import { publicTemplatesApi } from '@/api/publicTemplates' import type { PublicFlowTemplate, PublicScriptTemplate, PublicFlowDetail, PublicScriptDetail, PublicGalleryResponse, } from '@/types' type SortOption = 'most_used' | 'newest' | 'highest_success' type TypeFilter = 'all' | 'flows' | 'scripts' const sortLabels: Record = { most_used: 'Most Used', newest: 'Newest', highest_success: 'Highest Success Rate', } export default function PublicTemplatesPage() { const [searchParams, setSearchParams] = useSearchParams() // State const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filters const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '') const [activeCategory, setActiveCategory] = useState(searchParams.get('category') || '') const [typeFilter, setTypeFilter] = useState( (searchParams.get('type') as TypeFilter) || 'all' ) const [sort, setSort] = useState( (searchParams.get('sort') as SortOption) || 'most_used' ) const [sortOpen, setSortOpen] = useState(false) // Detail modal const [selectedFlow, setSelectedFlow] = useState(null) const [selectedScript, setSelectedScript] = useState(null) const [detailLoading, setDetailLoading] = useState(false) const searchTimeout = useRef | null>(null) // Fetch gallery data const fetchGallery = useCallback(async () => { setLoading(true) setError(null) try { const result = await publicTemplatesApi.listGallery({ category: activeCategory || undefined, type: typeFilter === 'all' ? undefined : typeFilter, sort, }) setData(result) } catch { setError('Failed to load templates. Please try again.') } finally { setLoading(false) } }, [activeCategory, typeFilter, sort]) // Search const doSearch = useCallback(async (q: string) => { if (!q.trim()) { fetchGallery() return } setLoading(true) setError(null) try { const result = await publicTemplatesApi.search(q.trim()) setData(result) } catch { setError('Search failed. Please try again.') } finally { setLoading(false) } }, [fetchGallery]) // Initial load & filter changes useEffect(() => { if (!searchQuery.trim()) { fetchGallery() } }, [fetchGallery, searchQuery]) // Sync filters to URL useEffect(() => { const params = new URLSearchParams() if (searchQuery) params.set('q', searchQuery) if (activeCategory) params.set('category', activeCategory) if (typeFilter !== 'all') params.set('type', typeFilter) if (sort !== 'most_used') params.set('sort', sort) setSearchParams(params, { replace: true }) }, [searchQuery, activeCategory, typeFilter, sort, setSearchParams]) // Debounced search const handleSearchChange = (value: string) => { setSearchQuery(value) if (searchTimeout.current) clearTimeout(searchTimeout.current) searchTimeout.current = setTimeout(() => { doSearch(value) }, 400) } // Open detail modal const openFlowDetail = async (template: PublicFlowTemplate) => { setDetailLoading(true) try { const detail = await publicTemplatesApi.getFlowDetail(template.id) setSelectedFlow(detail) } catch { // Fallback to the list data setSelectedFlow(template) } finally { setDetailLoading(false) } } const openScriptDetail = async (template: PublicScriptTemplate) => { setDetailLoading(true) try { const detail = await publicTemplatesApi.getScriptDetail(template.id) setSelectedScript(detail) } catch { // Script list items don't have full detail — show what we have setSelectedScript({ ...template, parameters: [], }) } finally { setDetailLoading(false) } } const categories = data?.categories || [] const flows = data?.flow_templates || [] const scripts = data?.script_templates || [] const showFlows = typeFilter === 'all' || typeFilter === 'flows' const showScripts = typeFilter === 'all' || typeFilter === 'scripts' return ( <>
{/* Subtle ambient glow */}
{/* Header */}
Resolution Flow
Sign In Sign Up Free
{/* Hero */}

MSP Troubleshooting{' '} Templates

Battle-tested flows and scripts built by MSP engineers. Free to browse, powerful when connected to FlowPilot.

{/* Search */}
handleSearchChange(e.target.value)} className="w-full pl-12 pr-4 py-3.5 rounded-2xl text-sm bg-card border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(96,165,250,0.3)] transition-colors" />
{/* Filters */}
{/* Category pills */} {categories.length > 0 && (
{categories.map((cat) => ( ))}
)} {/* Type toggle + Sort */}
{/* Type segmented control */}
{(['all', 'flows', 'scripts'] as TypeFilter[]).map((t) => ( ))}
{/* Sort dropdown */}
{sortOpen && ( <>
setSortOpen(false)} />
{(Object.entries(sortLabels) as [SortOption, string][]).map(([key, label]) => ( ))}
)}
{/* Content */}
{/* Loading */} {loading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)} {/* Error */} {!loading && error && (

{error}

)} {/* Empty */} {!loading && !error && flows.length === 0 && scripts.length === 0 && (

No templates found. Try a different search or category.

)} {/* Results */} {!loading && !error && (flows.length > 0 || scripts.length > 0) && (
{/* Flows */} {showFlows && flows.length > 0 && (
{typeFilter === 'all' && (

Flows ({data?.total_flows || flows.length})

)}
{flows.map((flow) => ( openFlowDetail(flow)} /> ))}
)} {/* Scripts */} {showScripts && scripts.length > 0 && (
{typeFilter === 'all' && (

Scripts ({data?.total_scripts || scripts.length})

)}
{scripts.map((script) => ( openScriptDetail(script)} /> ))}
)}
)}
{/* Footer */}
Powered by ResolutionFlow
Get Started
{/* Detail loading overlay */} {detailLoading && (
)} {/* Detail modals */} {selectedFlow && ( setSelectedFlow(null)} /> )} {selectedScript && ( setSelectedScript(null)} /> )}
) }