Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import type { GuideSummary } from '@/lib/dev-tools/types'; import { Icon } from '@/components/ui/icons'; export default function GuidesViewerPage() { const [guides, setGuides] = useState<GuideSummary[]>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { const fetchGuides = async () => { try { const res = await fetch('/api/admin/dev-tools/docs/guides'); if (!res.ok) throw new Error('Failed to fetch guides'); const result = await res.json(); // Handle both new wrapped format and legacy format const data = result.data ?? result; setGuides(data.guides); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load guides'); } finally { setIsLoading(false); } }; fetchGuides(); }, []); const filteredGuides = searchTerm ? guides.filter(guide => guide.title.toLowerCase().includes(searchTerm.toLowerCase()) || guide.slug.toLowerCase().includes(searchTerm.toLowerCase()) || guide.description?.toLowerCase().includes(searchTerm.toLowerCase()) ) : guides; if (isLoading) { return ( <div className="max-w-[1170px] mx-auto px-4 sm:px-7.5 xl:px-0"> <div className="animate-pulse"> <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48 mb-4"></div> <div className="space-y-4"> {[1, 2, 3].map(i => ( <div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div> ))} </div> </div> </div> ); } if (error) { return ( <div className="max-w-[1170px] mx-auto px-4 sm:px-7.5 xl:px-0"> <div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300"> {error} </div> </div> ); } return ( <div className="max-w-[1170px] mx-auto px-4 sm:px-7.5 xl:px-0"> {/* Breadcrumb */} <nav className="mb-4"> <ol className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"> <li><Link href="/admin/dev-tools" className="hover:text-blue">Developer Tools</Link></li> <li>/</li> <li className="text-dark dark:text-gray-100 font-medium">Guides</li> </ol> </nav> <div className="mb-8"> <h1 className="text-2xl font-bold text-dark dark:text-gray-100">Documentation Guides</h1> <p className="text-gray-600 dark:text-gray-400 mt-1"> Browse development and integration guides </p> </div> {/* Search */} <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6"> <div className="flex flex-wrap gap-4 items-center"> <div className="flex-1 min-w-[200px]"> <input type="text" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search guides..." className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue focus:border-transparent" /> </div> <div className="text-sm text-gray-600 dark:text-gray-400"> {filteredGuides.length} guide{filteredGuides.length !== 1 ? 's' : ''} </div> </div> </div> {/* Guide List */} {filteredGuides.length === 0 ? ( <div className="text-center py-12 text-gray-500 dark:text-gray-400"> {searchTerm ? 'No guides match your search' : 'No guides found'} </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {filteredGuides.map(guide => ( <Link key={guide.slug} href={`/admin/dev-tools/guides/${guide.slug}`} className="block bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-5 hover:shadow-md hover:border-blue dark:hover:border-blue transition-all" > <div className="flex items-start gap-4"> <div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg text-green-600 dark:text-green-400 flex-shrink-0"> <Icon name="file-text" size={20} /> </div> <div className="flex-1 min-w-0"> <h3 className="font-medium text-dark dark:text-gray-100">{guide.title}</h3> {guide.description && ( <p className="text-sm text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{guide.description}</p> )} <p className="text-xs text-gray-400 dark:text-gray-500 mt-2">{guide.filename}</p> </div> </div> </Link> ))} </div> )} </div> ); } |