* feat: implement light/dark mode theme system - Add semantic color CSS variables (success, warning, info, error) for both themes - Create ThemeProvider with React context and localStorage persistence - Add ThemeToggle component with sun/moon icons for header region - Add theme switcher in General Settings modal - Replace 200+ hardcoded Tailwind colors with CSS variables across 30+ components - Update layout.tsx to remove forced dark mode - Keep terminal colors unchanged as requested - Default to dark mode, with seamless light/dark switching Components updated: - High-priority: InstalledScriptsTab, ScriptInstallationCard, LXCSettingsModal, ScriptsGrid - All remaining component files with hardcoded colors - UI components: button, toggle, badge variants - Modal components: ErrorModal, ConfirmationModal, AuthModal, SetupModal - Form components: ServerForm, FilterBar, CategorySidebar - Display components: ScriptCard, ScriptCardList, DiffViewer, TextViewer Theme switchers: - Header: Small nuanced toggle in top-right - Settings: Detailed Light/Dark selection in General Settings * fix: resolve ESLint warnings - Fix missing dependencies in useCallback and useEffect hooks - Prefix unused parameter with underscore to satisfy ESLint rules - Build now completes without warnings * fix: improve toggle component styling for better visibility - Use explicit gray colors instead of CSS variables for toggle background - Ensure proper contrast in both light and dark modes - Toggle switches now display correctly with proper visual states * fix: improve toggle visual states for better UX - Use explicit conditional styling instead of peer classes - Active toggles now clearly show primary color background - Inactive toggles show gray background for clear distinction - Much easier to tell which toggles are on/off at a glance * fix: improve toggle contrast in dark mode - Change inactive toggle background from gray-700 to gray-600 for better visibility - Add darker border color (gray-500) for toggle handle in dark mode - Toggles now have proper contrast against dark backgrounds - Both light and dark modes now have clear visual distinction * fix: resolve dependency loop and improve dropdown styling - Fix circular dependency in InstalledScriptsTab status check - Remove fetchContainerStatuses function and inline logic in useEffect - Make all dropdown menu items grey with consistent hover effects - Update both ScriptInstallationCard and InstalledScriptsTab dropdowns - Remove unused useCallback import - Build now completes without warnings or errors * fix: restore proper button colors and eliminate dependency loop - Restore red color for Stop/Destroy buttons and green for Start buttons - Fix circular dependency by using ref for containerStatusMutation - Update both InstalledScriptsTab and ScriptInstallationCard dropdowns - Maintain grey color for other menu items (Update, Shell, Open UI, etc.) - Build now completes without warnings or dependency loops * feat: add missing hover utility classes for semantic colors - Add hover states for success, warning, info, error colors - Add hover:bg-success/20, hover:bg-error/20, etc. classes - Add hover:text-success-foreground, hover:text-error-foreground classes - Start/Stop and Destroy buttons now have proper hover effects - All dropdown menu items now have consistent hover behavior * feat: improve status cards with useful LXC container information - Replace useless 'Successful/Failed/In Progress' cards with meaningful data - Show 'Running LXC' count in green (actual running containers) - Show 'Stopped LXC' count in red (actual stopped containers) - Keep 'Total Installations' for overall count - Change layout from 4 columns to 3 columns for better spacing - Status cards now show real-time container states instead of installation status * style: center content in status cards - Add text-center class to each individual status card - Numbers and labels now centered within each card - Improves visual balance and readability - All three cards (Total, Running LXC, Stopped LXC) now have centered content
127 lines
4.7 KiB
TypeScript
127 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import Image from 'next/image';
|
|
import type { ScriptCard } from '~/types/script';
|
|
import { TypeBadge, UpdateableBadge } from './Badge';
|
|
|
|
interface ScriptCardProps {
|
|
script: ScriptCard;
|
|
onClick: (script: ScriptCard) => void;
|
|
isSelected?: boolean;
|
|
onToggleSelect?: (slug: string) => void;
|
|
}
|
|
|
|
export function ScriptCard({ script, onClick, isSelected = false, onToggleSelect }: ScriptCardProps) {
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
const handleImageError = () => {
|
|
setImageError(true);
|
|
};
|
|
|
|
const handleCheckboxClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
if (onToggleSelect && script.slug) {
|
|
onToggleSelect(script.slug);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="bg-card rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-border hover:border-primary h-full flex flex-col relative"
|
|
onClick={() => onClick(script)}
|
|
>
|
|
{/* Checkbox in top-left corner */}
|
|
{onToggleSelect && (
|
|
<div className="absolute top-2 left-2 z-10">
|
|
<div
|
|
className={`w-4 h-4 border-2 rounded cursor-pointer transition-all duration-200 flex items-center justify-center ${
|
|
isSelected
|
|
? 'bg-primary border-primary text-primary-foreground'
|
|
: 'bg-card border-border hover:border-primary/60 hover:bg-accent'
|
|
}`}
|
|
onClick={handleCheckboxClick}
|
|
>
|
|
{isSelected && (
|
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-6 flex-1 flex flex-col">
|
|
{/* Header with logo and name */}
|
|
<div className="flex items-start space-x-4 mb-4">
|
|
<div className="flex-shrink-0">
|
|
{script.logo && !imageError ? (
|
|
<Image
|
|
src={script.logo}
|
|
alt={`${script.name} logo`}
|
|
width={48}
|
|
height={48}
|
|
className="w-12 h-12 rounded-lg object-contain"
|
|
onError={handleImageError}
|
|
/>
|
|
) : (
|
|
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center">
|
|
<span className="text-muted-foreground text-lg font-semibold">
|
|
{script.name?.charAt(0)?.toUpperCase() || '?'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-lg font-semibold text-foreground truncate">
|
|
{script.name || 'Unnamed Script'}
|
|
</h3>
|
|
<div className="mt-2 space-y-2">
|
|
{/* Type and Updateable status on first row */}
|
|
<div className="flex items-center space-x-2 flex-wrap gap-1">
|
|
<TypeBadge type={script.type ?? 'unknown'} />
|
|
{script.updateable && <UpdateableBadge />}
|
|
</div>
|
|
|
|
{/* Download Status */}
|
|
<div className="flex items-center space-x-1">
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
script.isDownloaded ? 'bg-success' : 'bg-error'
|
|
}`}></div>
|
|
<span className={`text-xs font-medium ${
|
|
script.isDownloaded ? 'text-success' : 'text-error'
|
|
}`}>
|
|
{script.isDownloaded ? 'Downloaded' : 'Not Downloaded'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<p className="text-muted-foreground text-sm line-clamp-3 mb-4 flex-1">
|
|
{script.description || 'No description available'}
|
|
</p>
|
|
|
|
{/* Footer with website link */}
|
|
{script.website && (
|
|
<div className="mt-auto">
|
|
<a
|
|
href={script.website}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-info hover:text-info/80 text-sm font-medium flex items-center space-x-1"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<span>Website</span>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|