feat: Add GitHub script list functionality with cards and modal
- Add JSON_FOLDER environment variable for GitHub repo JSON path - Create TypeScript types for script JSON structure - Implement GitHub API service to fetch scripts from repository - Add tRPC routes for script management (getScriptCards, getAllScripts, getScriptBySlug, resyncScripts) - Create ScriptCard component for displaying script information - Create ScriptDetailModal for full script details view - Create ScriptsGrid component with loading and error states - Add ResyncButton component for refreshing scripts from upstream - Update main page with tabbed interface (GitHub Scripts vs Local Scripts) - Add proper error handling and loading states throughout
This commit is contained in:
98
src/app/_components/ScriptsGrid.tsx
Normal file
98
src/app/_components/ScriptsGrid.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
import { ScriptCard } from './ScriptCard';
|
||||
import { ScriptDetailModal } from './ScriptDetailModal';
|
||||
import type { ScriptCard as ScriptCardType, Script } from '~/types/script';
|
||||
|
||||
export function ScriptsGrid() {
|
||||
const [selectedScript, setSelectedScript] = useState<Script | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const { data: scriptCardsData, isLoading, error, refetch } = api.scripts.getScriptCards.useQuery();
|
||||
const { data: scriptData } = api.scripts.getScriptBySlug.useQuery(
|
||||
{ slug: selectedScript?.slug ?? '' },
|
||||
{ enabled: !!selectedScript }
|
||||
);
|
||||
|
||||
const handleCardClick = (scriptCard: ScriptCardType) => {
|
||||
// We'll fetch the full script data when the modal opens
|
||||
setSelectedScript(scriptCard as any); // Temporary cast, will be replaced with full script data
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-2 text-gray-600">Loading scripts...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !scriptCardsData?.success) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-red-600 mb-4">
|
||||
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">Failed to load scripts</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{scriptCardsData?.error || 'Unknown error occurred'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const scripts = scriptCardsData.cards || [];
|
||||
|
||||
if (scripts.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">
|
||||
<svg className="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">No scripts found</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
No script files were found in the repository.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{scripts.map((script) => (
|
||||
<ScriptCard
|
||||
key={script.slug}
|
||||
script={script}
|
||||
onClick={handleCardClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ScriptDetailModal
|
||||
script={scriptData?.success ? scriptData.script : null}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user