refactor: use ternary expressions for conditional JSX

Replace `{condition && (<JSX>)}` pattern with
`{condition ? (<JSX>) : null}` across all frontend components.
This avoids potential falsy value rendering issues with
the logical AND operator.
This commit is contained in:
Darkraise
2026-02-04 05:50:59 +07:00
parent a263db1a2f
commit 5ca30044d6
72 changed files with 472 additions and 493 deletions
+2 -2
View File
@@ -189,14 +189,14 @@ function App() {
<Route path="/error/500" element={<ServerErrorView />} />
<Route path="/error/network" element={<NetworkErrorView />} />
<Route path="*" element={<NotFoundView />} />
{import.meta.env.DEV && (
{import.meta.env.DEV ? (
<>
<Route path="/_dev/404" element={<NotFoundView />} />
<Route path="/_dev/500" element={<ServerErrorView />} />
<Route path="/_dev/network" element={<NetworkErrorView />} />
<Route path="/_dev/403" element={<UnauthorizedView />} />
</>
)}
) : null}
</Route>
{/* ==================== STANDALONE ==================== */}
@@ -93,9 +93,9 @@ const ACTIVITY_TABLE_COLUMNS: ColumnDef<ActivityLog>[] = [
return (
<div className="text-muted-foreground">
{resource || '-'}
{resourceId && (
{resourceId ? (
<span className="text-muted-foreground/60"> #{resourceId}</span>
)}
) : null}
</div>
);
},
@@ -232,14 +232,14 @@ function ActivityTableComponent() {
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2">
{showFilters ? 'Hide' : 'Show'} Filters
{!showFilters && activeFilterCount > 0 && (
{!showFilters && activeFilterCount > 0 ? (
<Badge
variant="default"
className="h-5 min-w-[20px] px-1.5 text-xs font-semibold"
>
{activeFilterCount}
</Badge>
)}
) : null}
<ChevronDown
className={`h-4 w-4 transition-transform ${
showFilters ? 'rotate-180' : ''
@@ -247,11 +247,11 @@ function ActivityTableComponent() {
/>
</Button>
</CollapsibleTrigger>
{hasActiveFilters && (
{hasActiveFilters ? (
<Button variant="ghost" size="sm" onClick={handleClearFilters}>
Clear Filters
</Button>
)}
) : null}
</div>
<CollapsibleContent className="mt-4">
<div className="rounded-lg border bg-card p-4 space-y-4">
@@ -317,12 +317,12 @@ function ActivityTableComponent() {
</div>
</div>
{filterState.dateRangeError && (
{filterState.dateRangeError ? (
<div className="text-sm text-destructive flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
{filterState.dateRangeError}
</div>
)}
) : null}
</div>
</div>
</CollapsibleContent>
@@ -42,7 +42,7 @@ function ActivityDashboardComponent() {
{isCollapsed ? <ChevronDown size={20} /> : <ChevronUp size={20} />}
</button>
<h2 className="text-xl font-bold">Activity Dashboard</h2>
{!isCollapsed && (
{!isCollapsed ? (
<div className="flex gap-2 ml-4">
<button
onClick={() => setTimeRange('24h')}
@@ -75,9 +75,9 @@ function ActivityDashboardComponent() {
30d
</button>
</div>
)}
) : null}
</div>
{!isCollapsed && (
{!isCollapsed ? (
<button
onClick={handleToggleAutoRefresh}
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
@@ -92,18 +92,18 @@ function ActivityDashboardComponent() {
/>
<span className="text-sm">
Auto-refresh {autoRefresh ? 'ON' : 'OFF'}
{autoRefresh && isFetching && <span className="ml-1 opacity-70">(updating...)</span>}
{autoRefresh && isFetching ? <span className="ml-1 opacity-70">(updating...)</span> : null}
</span>
</button>
)}
) : null}
</div>
</div>
{/* Dashboard Content */}
{!isCollapsed && (
{!isCollapsed ? (
<div className="space-y-6">
{/* Loading State */}
{isLoading && (
{isLoading ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="animate-pulse space-y-4">
<div className="h-24 bg-muted rounded"></div>
@@ -114,15 +114,15 @@ function ActivityDashboardComponent() {
</div>
</div>
</div>
)}
) : null}
{/* Stats Cards */}
{stats && (
{stats ? (
<ActivityStats
stats={stats.data}
timeRange={timeRange}
/>
)}
) : null}
{/* Activity Trend Chart */}
<ActivityTrendChart autoRefresh={autoRefresh} />
@@ -139,7 +139,7 @@ function ActivityDashboardComponent() {
{/* User Leaderboard */}
<UserLeaderboard autoRefresh={autoRefresh} />
</div>
)}
) : null}
</div>
);
}
@@ -116,11 +116,11 @@ export function ActivityStats({ stats, timeRange = '24h' }: ActivityStatsProps)
{stat.value}
</p>
<p className="text-muted-foreground text-xs mt-1 truncate">{stat.sublabel}</p>
{stat.trend !== undefined && (
{stat.trend !== undefined ? (
<div className={`text-xs mt-1 font-medium ${getTrendColor(stat.trend)}`}>
{getTrendIcon(stat.trend)} {Math.abs(stat.trend)}%
</div>
)}
) : null}
</div>
</div>
</div>
@@ -25,12 +25,12 @@ function CustomTooltip({ active, payload }: CustomTooltipProps) {
<span className="text-muted-foreground text-sm">Percentage:</span>
<span className="font-semibold text-sm">{percentage.toFixed(1)}%</span>
</div>
{resourceData.topAction && (
{resourceData.topAction ? (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm">Top Action:</span>
<span className="font-semibold text-sm">{resourceData.topAction}</span>
</div>
)}
) : null}
</div>
</div>
);
@@ -69,7 +69,7 @@ export function ResourceDistributionChart({ autoRefresh = false }: ResourceDistr
const { payload } = props;
return (
<div className="flex flex-wrap gap-2 justify-center mt-4 max-h-24 overflow-y-auto">
{payload?.map((entry, index: number) => (
{payload?.map((entry, index: number) =>
entry.value ? (
<div key={`legend-${index}`} className="flex items-center gap-2 text-xs">
<div
@@ -79,7 +79,7 @@ export function ResourceDistributionChart({ autoRefresh = false }: ResourceDistr
<span className="text-muted-foreground">{entry.value}</span>
</div>
) : null
))}
)}
</div>
);
};
@@ -25,12 +25,12 @@ function CustomTooltip({ active, payload }: CustomTooltipProps) {
<span className="text-muted-foreground text-sm">Category:</span>
<span className="font-semibold text-sm capitalize">{action.category}</span>
</div>
{action.topResource && (
{action.topResource ? (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm">Top Resource:</span>
<span className="font-semibold text-sm">{action.topResource}</span>
</div>
)}
) : null}
</div>
</div>
);
+10 -10
View File
@@ -140,7 +140,7 @@ export function LoginForm() {
</div>
{/* Setup Complete Notice */}
{setupComplete && !error && (
{setupComplete && !error ? (
<Alert
className="mb-6 border-green-500/50 bg-green-500/10 text-green-900 dark:text-green-100 animate-slide-in-down"
role="alert"
@@ -152,10 +152,10 @@ export function LoginForm() {
Your administrator account has been created successfully. Please sign in to continue.
</AlertDescription>
</Alert>
)}
) : null}
{/* Maintenance Mode Notice */}
{publicSettings?.app?.maintenanceMode && (
{publicSettings?.app?.maintenanceMode ? (
<Alert
className="mb-6 border-amber-500/50 bg-amber-500/10 text-amber-900 dark:text-amber-100 animate-slide-in-down"
role="alert"
@@ -168,9 +168,9 @@ export function LoginForm() {
'The system is currently under maintenance. Some features may be unavailable.'}
</AlertDescription>
</Alert>
)}
) : null}
{error && (
{error ? (
<Alert
variant="destructive"
className="mb-6 animate-slide-in-down"
@@ -179,7 +179,7 @@ export function LoginForm() {
>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="relative space-y-4">
@@ -243,7 +243,7 @@ export function LoginForm() {
)}
</Button>
{authSettings?.guestAccessEnabled && (
{authSettings?.guestAccessEnabled ? (
<Button
type="button"
variant="outline"
@@ -252,12 +252,12 @@ export function LoginForm() {
>
Browse as Guest
</Button>
)}
) : null}
</div>
</form>
</Form>
{authSettings?.userRegistrationEnabled && (
{authSettings?.userRegistrationEnabled ? (
<div className="relative mt-6 pt-4 border-t border-border/50">
<p className="text-center text-sm text-muted-foreground">
Don't have an account?{' '}
@@ -269,7 +269,7 @@ export function LoginForm() {
</Link>
</p>
</div>
)}
) : null}
</div>
</div>
</div>
@@ -144,7 +144,7 @@ export function PrismaticBackground() {
/>
{/* Light Layer 3: Cool analogous - Desktop only, vertical wave */}
{!isMobile && (
{!isMobile ? (
<div
className="prismatic-light-3"
style={{
@@ -156,7 +156,7 @@ export function PrismaticBackground() {
}}
aria-hidden="true"
/>
)}
) : null}
</>
);
}
@@ -197,7 +197,7 @@ export function RegisterForm() {
</div>
{/* Maintenance Mode Notice */}
{publicSettings?.app?.maintenanceMode && (
{publicSettings?.app?.maintenanceMode ? (
<Alert
className="mb-6 border-amber-500/50 bg-amber-500/10 text-amber-900 dark:text-amber-100"
role="alert"
@@ -210,15 +210,15 @@ export function RegisterForm() {
'The system is currently under maintenance. Some features may be unavailable.'}
</AlertDescription>
</Alert>
)}
) : null}
{registerMutation.isError && (
{registerMutation.isError ? (
<Alert variant="destructive" className="mb-6">
<AlertDescription>
{getErrorMessage(registerMutation.error) || 'Registration failed. Please try again.'}
</AlertDescription>
</Alert>
)}
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="relative space-y-4">
@@ -30,7 +30,7 @@ export function Breadcrumbs({
return (
<nav aria-label="Breadcrumb" className={cn("flex items-center gap-2 text-sm", className)}>
<ol className="flex items-center gap-2 flex-wrap">
{showHome && (
{showHome ? (
<>
<li>
<Link
@@ -42,13 +42,13 @@ export function Breadcrumbs({
<span className="hidden sm:inline">{homeLabel}</span>
</Link>
</li>
{items.length > 0 && (
{items.length > 0 ? (
<li aria-hidden="true">
<ChevronRight size={16} className="text-muted-foreground" />
</li>
)}
) : null}
</>
)}
) : null}
{items.map((item, index) => {
const isLast = index === items.length - 1;
@@ -79,9 +79,9 @@ export function Breadcrumbs({
</span>
)}
{!isLast && (
{!isLast ? (
<ChevronRight size={16} className="text-muted-foreground" aria-hidden="true" />
)}
) : null}
</li>
);
})}
@@ -36,7 +36,7 @@ export function CardSizeControl() {
</div>
{/* Grid Size Controls */}
{viewMode === 'grid' && (
{viewMode === 'grid' ? (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Size:</span>
<ToggleGroup type="single" value={cardSize} onValueChange={(value: string) => value && setCardSize(value as CardSize)}>
@@ -48,10 +48,10 @@ export function CardSizeControl() {
))}
</ToggleGroup>
</div>
)}
) : null}
{/* List Column Controls */}
{viewMode === 'list' && (
{viewMode === 'list' ? (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Columns:</span>
<ToggleGroup type="single" value={listColumns.toString()} onValueChange={(value: string) => value && setListColumns(parseInt(value) as ListColumns)}>
@@ -63,7 +63,7 @@ export function CardSizeControl() {
))}
</ToggleGroup>
</div>
)}
) : null}
</div>
);
}
@@ -88,20 +88,20 @@ export class ErrorBoundary extends Component<Props, State> {
</div>
{/* Error details (only in development) */}
{process.env.NODE_ENV === 'development' && this.state.error && (
{process.env.NODE_ENV === 'development' && this.state.error ? (
<div className="mb-6 p-4 bg-background-primary rounded-lg border border-border">
<h2 className="text-sm font-semibold text-red-400 mb-2">Error Details:</h2>
<pre className="text-xs text-gray-300 overflow-auto max-h-64">
{this.state.error.toString()}
{this.state.errorInfo && (
{this.state.errorInfo ? (
<>
{'\n\nComponent Stack:'}
{this.state.errorInfo.componentStack}
</>
)}
) : null}
</pre>
</div>
)}
) : null}
{/* Action buttons */}
<div className="flex gap-3">
@@ -79,11 +79,11 @@ export function FavoriteButton({
className={toggleFavorite.isPending ? "animate-pulse" : ""}
aria-hidden="true"
/>
{showLabel && (
{showLabel ? (
<span className="ml-2">
{isFavorited ? "Favorited" : "Favorite"}
</span>
)}
) : null}
</Button>
);
}
@@ -44,7 +44,7 @@ export class ActivityErrorBoundary extends Component<Props, State> {
<p className="text-sm text-muted-foreground mb-4">
An error occurred while loading the activity logs. Please try refreshing the page.
</p>
{this.state.error && (
{this.state.error ? (
<details className="text-xs text-muted-foreground mb-4">
<summary className="cursor-pointer hover:text-foreground">
Error details
@@ -53,7 +53,7 @@ export class ActivityErrorBoundary extends Component<Props, State> {
{this.state.error.toString()}
</pre>
</details>
)}
) : null}
<button
onClick={this.handleReset}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
+12 -12
View File
@@ -138,11 +138,11 @@ export function ErrorPage({
<div className="space-y-2">
<div className="flex items-center justify-center gap-2">
<CardTitle className="text-2xl font-bold">{displayTitle}</CardTitle>
{errorCode && (
{errorCode ? (
<Badge variant={config.badgeVariant}>
{errorCode}
</Badge>
)}
) : null}
</div>
<p className="text-muted-foreground">{displayMessage}</p>
</div>
@@ -150,7 +150,7 @@ export function ErrorPage({
</CardHeader>
<CardContent className="space-y-4">
{hasDetails && (
{hasDetails ? (
<Alert>
<AlertDescription>
<div className="space-y-2">
@@ -170,15 +170,15 @@ export function ErrorPage({
</>
)}
</button>
{showDetails && (
{showDetails ? (
<pre className="mt-2 p-4 bg-muted rounded-md overflow-x-auto text-xs">
<code>{details}</code>
</pre>
)}
) : null}
</div>
</AlertDescription>
</Alert>
)}
) : null}
<div className="space-y-2">
<h3 className="font-semibold text-sm">What you can do:</h3>
@@ -198,21 +198,21 @@ export function ErrorPage({
<Button onClick={handleGoHome} variant="default">
Go to Home
</Button>
{onRetry && (
{onRetry ? (
<Button onClick={handleRetry} variant="secondary">
Retry
</Button>
)}
{showReportButton && onReport && !hasReported && (
) : null}
{showReportButton && onReport && !hasReported ? (
<Button onClick={handleReport} variant="outline" disabled={isReporting}>
{isReporting ? 'Reporting...' : 'Report Error'}
</Button>
)}
{hasReported && (
) : null}
{hasReported ? (
<span className="text-sm text-muted-foreground">
✓ Error reported successfully
</span>
)}
) : null}
</>
)}
</CardFooter>
@@ -126,26 +126,26 @@ export class GameLibraryErrorBoundary extends Component<Props, State> {
</div>
{/* Error details */}
{this.state.error && !this.state.isRetrying && (
{this.state.error && !this.state.isRetrying ? (
<div className="mb-6 p-4 bg-background-primary rounded-lg border border-border">
<h3 className="text-sm font-semibold text-destructive mb-2">Error Details:</h3>
<p className="text-sm text-muted-foreground font-mono">
{this.state.error.message}
</p>
</div>
)}
) : null}
{/* Retry information */}
{this.state.retryCount > 0 && !this.state.isRetrying && (
{this.state.retryCount > 0 && !this.state.isRetrying ? (
<div className="mb-6 p-4 bg-blue-500/10 rounded-lg border border-blue-500/20">
<p className="text-sm text-blue-400">
Attempted {this.state.retryCount} automatic {this.state.retryCount === 1 ? 'retry' : 'retries'}
</p>
</div>
)}
) : null}
{/* Action buttons */}
{!this.state.isRetrying && (
{!this.state.isRetrying ? (
<div className="flex gap-3">
<Button
variant="default"
@@ -156,15 +156,15 @@ export class GameLibraryErrorBoundary extends Component<Props, State> {
Try Again
</Button>
</div>
)}
) : null}
{/* Retry progress */}
{this.state.isRetrying && (
{this.state.isRetrying ? (
<div className="flex items-center gap-3 text-muted-foreground">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-primary border-t-transparent" />
<span className="text-sm">Retrying...</span>
</div>
)}
) : null}
{/* Help text */}
<div className="mt-6 pt-6 border-t border-border">
@@ -123,16 +123,16 @@ export class GamePlayerErrorBoundary extends Component<Props, State> {
? `Failed to load "${this.props.gameTitle}"`
: 'An error occurred while loading the game'}
</p>
{this.props.platform && (
{this.props.platform ? (
<p className="text-sm text-muted-foreground mt-1">
Platform: {this.props.platform}
</p>
)}
) : null}
</div>
</div>
{/* Error details */}
{this.state.error && !this.state.isRetrying && (
{this.state.error && !this.state.isRetrying ? (
<div className="mb-6 p-4 bg-background-primary rounded-lg border border-border">
<h3 className="text-sm font-semibold text-destructive mb-2">Error Details:</h3>
<p className="text-sm text-muted-foreground font-mono">
@@ -140,44 +140,44 @@ export class GamePlayerErrorBoundary extends Component<Props, State> {
</p>
{/* Common error patterns */}
{this.state.error.message.includes('Failed to fetch') && (
{this.state.error.message.includes('Failed to fetch') ? (
<div className="mt-3 p-3 bg-yellow-500/10 rounded border border-yellow-500/20">
<p className="text-sm text-yellow-400">
<strong>Network Error:</strong> Unable to download game files. Check your internet connection.
</p>
</div>
)}
) : null}
{this.state.error.message.includes('404') && (
{this.state.error.message.includes('404') ? (
<div className="mt-3 p-3 bg-yellow-500/10 rounded border border-yellow-500/20">
<p className="text-sm text-yellow-400">
<strong>File Not Found:</strong> Game files may be missing or not downloaded yet.
</p>
</div>
)}
) : null}
{this.props.platform === 'Flash' && this.state.error.message.includes('Ruffle') && (
{this.props.platform === 'Flash' && this.state.error.message.includes('Ruffle') ? (
<div className="mt-3 p-3 bg-blue-500/10 rounded border border-blue-500/20">
<p className="text-sm text-blue-400">
<strong>Ruffle Error:</strong> The Flash emulator encountered an issue. Try refreshing the page.
</p>
</div>
)}
) : null}
</div>
)}
) : null}
{/* Retry count */}
{this.state.retryCount > 0 && !this.state.isRetrying && (
{this.state.retryCount > 0 && !this.state.isRetrying ? (
<div className="mb-6 p-4 bg-blue-500/10 rounded-lg border border-blue-500/20">
<p className="text-sm text-blue-400">
Attempted {this.state.retryCount} {this.state.retryCount === 1 ? 'retry' : 'retries'}
</p>
</div>
)}
) : null}
{/* Action buttons */}
<div className="flex flex-wrap gap-3 mb-6">
{canRetry && !this.state.isRetrying && (
{canRetry && !this.state.isRetrying ? (
<Button
variant="default"
onClick={this.handleRetry}
@@ -186,13 +186,13 @@ export class GamePlayerErrorBoundary extends Component<Props, State> {
<RefreshCw size={18} className="mr-2" />
Try Again
</Button>
)}
{this.state.isRetrying && (
) : null}
{this.state.isRetrying ? (
<Button variant="default" disabled>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2" />
Retrying...
</Button>
)}
) : null}
<Button
variant="secondary"
onClick={this.handleBack}
@@ -217,7 +217,7 @@ export class GamePlayerErrorBoundary extends Component<Props, State> {
</ul>
{/* Download suggestion for unsupported games */}
{this.props.platform && !['Flash', 'HTML5'].includes(this.props.platform) && (
{this.props.platform && !['Flash', 'HTML5'].includes(this.props.platform) ? (
<div className="mt-4 p-3 bg-blue-500/10 rounded border border-blue-500/20">
<p className="text-sm text-blue-400 flex items-start gap-2">
<Download size={16} className="mt-0.5 flex-shrink-0" />
@@ -226,7 +226,7 @@ export class GamePlayerErrorBoundary extends Component<Props, State> {
</span>
</p>
</div>
)}
) : null}
</div>
</div>
</div>
@@ -70,7 +70,7 @@ export function NetworkErrorView() {
return (
<div className="relative">
{showRestored && (
{showRestored ? (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50">
<Alert className="bg-green-50 border-green-200">
<CheckCircle2 className="h-4 w-4 text-green-600" />
@@ -79,7 +79,7 @@ export function NetworkErrorView() {
</AlertDescription>
</Alert>
</div>
)}
) : null}
<ErrorPage
type="network"
@@ -86,7 +86,7 @@ export class PlaylistErrorBoundary extends Component<Props, State> {
</div>
{/* Error details */}
{this.state.error && (
{this.state.error ? (
<div className="mb-6 p-4 bg-background-primary rounded-lg border border-border">
<h3 className="text-sm font-semibold text-destructive mb-2">Error Details:</h3>
<p className="text-sm text-muted-foreground font-mono">
@@ -94,31 +94,31 @@ export class PlaylistErrorBoundary extends Component<Props, State> {
</p>
{/* Common playlist errors */}
{this.state.error.message.includes('404') && (
{this.state.error.message.includes('404') ? (
<p className="text-sm text-muted-foreground mt-2">
This playlist may have been deleted or you don't have permission to access it.
</p>
)}
{this.state.error.message.includes('403') && (
) : null}
{this.state.error.message.includes('403') ? (
<p className="text-sm text-muted-foreground mt-2">
You don't have permission to access this playlist.
</p>
)}
) : null}
</div>
)}
) : null}
{/* Retry count */}
{this.state.retryCount > 0 && (
{this.state.retryCount > 0 ? (
<div className="mb-6 p-4 bg-blue-500/10 rounded-lg border border-blue-500/20">
<p className="text-sm text-blue-400">
Attempted {this.state.retryCount} {this.state.retryCount === 1 ? 'retry' : 'retries'}
</p>
</div>
)}
) : null}
{/* Action buttons */}
<div className="flex gap-3">
{canRetry && (
{canRetry ? (
<Button
variant="default"
onClick={this.handleRetry}
@@ -126,7 +126,7 @@ export class PlaylistErrorBoundary extends Component<Props, State> {
<RefreshCw size={18} className="mr-2" />
Try Again
</Button>
)}
) : null}
<Button
variant="secondary"
onClick={this.handleBack}
+18 -18
View File
@@ -91,14 +91,14 @@ export function JobCard({
{/* BODY: Schedule, Last Execution, Next Run, Running Status */}
<CardContent className="space-y-4 pt-3 flex-1 overflow-auto">
{/* Running Status Badge - Only show when job is currently running */}
{job.running && (
{job.running ? (
<div className="bg-blue-500/10 border border-blue-500/20 rounded-md p-3">
<div className="flex items-center gap-2">
<Loader2 size={16} className="animate-spin text-blue-500" />
<span className="text-sm font-medium">Job is currently running</span>
</div>
</div>
)}
) : null}
{/* Schedule */}
<div>
@@ -125,22 +125,22 @@ export function JobCard({
})}
</span>
</div>
{job.lastExecution.durationSeconds !== undefined && (
{job.lastExecution.durationSeconds !== undefined ? (
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">Duration:</span>
<span className="font-medium">
{formatDuration(job.lastExecution.durationSeconds)}
</span>
</div>
)}
{job.lastExecution.message && (
) : null}
{job.lastExecution.message ? (
<div className="mt-2 text-xs space-y-1">
{job.lastExecution.message.includes("Games updated") ? (
// Parse metadata sync result
<div className="bg-muted/50 rounded p-2 space-y-0.5">
{job.lastExecution.message.match(
/Games updated: (\d+)/,
) && (
) ? (
<div className="flex justify-between">
<span className="text-muted-foreground">
Games updated:
@@ -153,10 +153,10 @@ export function JobCard({
}
</span>
</div>
)}
) : null}
{job.lastExecution.message.match(
/Tags updated: (\d+)/,
) && (
) ? (
<div className="flex justify-between">
<span className="text-muted-foreground">
Tags updated:
@@ -169,10 +169,10 @@ export function JobCard({
}
</span>
</div>
)}
) : null}
{job.lastExecution.message.match(
/Platforms updated: (\d+)/,
) && (
) ? (
<div className="flex justify-between">
<span className="text-muted-foreground">
Platforms updated:
@@ -185,10 +185,10 @@ export function JobCard({
}
</span>
</div>
)}
) : null}
{job.lastExecution.message.match(
/Games deleted: (\d+)/,
) && (
) ? (
<div className="flex justify-between">
<span className="text-muted-foreground">
Games deleted:
@@ -201,7 +201,7 @@ export function JobCard({
}
</span>
</div>
)}
) : null}
</div>
) : (
// Display as plain text for other messages
@@ -210,7 +210,7 @@ export function JobCard({
</p>
)}
</div>
)}
) : null}
</div>
) : (
<Badge variant="outline">Never run</Badge>
@@ -218,7 +218,7 @@ export function JobCard({
</div>
{/* Next Run */}
{job.enabled && job.nextRunEstimate && !job.running && (
{job.enabled && job.nextRunEstimate && !job.running ? (
<div>
<p className="text-xs font-semibold text-muted-foreground uppercase mb-1">
Next Run
@@ -229,12 +229,12 @@ export function JobCard({
})}
</p>
</div>
)}
) : null}
</CardContent>
{/* FOOTER: Action Buttons */}
<CardFooter className="flex gap-2 pt-4 border-t flex-wrap flex-shrink-0">
{job.running && (
{job.running ? (
<Button
onClick={() => onStop(job.id)}
variant="outline"
@@ -244,7 +244,7 @@ export function JobCard({
<Square size={16} className="mr-1" />
Stop
</Button>
)}
) : null}
<Button
onClick={() => onTrigger(job.id)}
variant="outline"
@@ -136,20 +136,20 @@ export function JobEditDialog({ job, open, onOpenChange }: JobEditDialogProps) {
/>
{/* Validation Error */}
{validationError && (
{validationError ? (
<div className="flex items-center gap-2 text-sm text-red-500">
<AlertCircle size={16} />
<span>{validationError}</span>
</div>
)}
) : null}
{/* Human-readable description */}
{!validationError && cronSchedule && (
{!validationError && cronSchedule ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Info size={16} />
<span>Will run: {cronToReadable(cronSchedule)}</span>
</div>
)}
) : null}
{/* Quick preset buttons */}
<div className="flex flex-wrap gap-2 mt-2">
@@ -64,14 +64,14 @@ export function JobExecutionLogTable({ logs, loading }: JobExecutionLogTableProp
<>
<tr key={log.id} className="border-t hover:bg-accent/50">
<td className="p-3">
{log.errorDetails && (
{log.errorDetails ? (
<button
onClick={() => toggleRow(log.id)}
className="hover:bg-accent p-1 rounded"
>
{expandedRows.has(log.id) ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
)}
) : null}
</td>
<td className="p-3">{getStatusBadge(log.status)}</td>
<td className="p-3 text-sm">
@@ -81,7 +81,7 @@ export function JobExecutionLogTable({ logs, loading }: JobExecutionLogTableProp
<td className="p-3 text-sm">{log.triggeredBy}</td>
<td className="p-3 text-sm text-muted-foreground">{log.message || '-'}</td>
</tr>
{expandedRows.has(log.id) && log.errorDetails && (
{expandedRows.has(log.id) && log.errorDetails ? (
<tr className="border-t bg-red-950/20">
<td colSpan={6} className="p-3">
<div className="text-xs font-mono bg-red-950/50 p-3 rounded">
@@ -90,7 +90,7 @@ export function JobExecutionLogTable({ logs, loading }: JobExecutionLogTableProp
</div>
</td>
</tr>
)}
) : null}
</>
))}
</tbody>
+7 -8
View File
@@ -104,7 +104,7 @@ export function Header({ hideSidebarToggle = false, hideSearch = false }: Header
</div>
</Link>
{!hideSidebarToggle && (
{!hideSidebarToggle ? (
<Button
variant="ghost"
size="icon"
@@ -118,15 +118,15 @@ export function Header({ hideSidebarToggle = false, hideSearch = false }: Header
>
<Menu size={20} />
</Button>
)}
) : null}
</div>
{/* Center: Search Bar (Desktop only) */}
{!hideSearch && (
{!hideSearch ? (
<div className="flex-1 max-w-2xl mx-auto hidden md:block">
<SearchBar />
</div>
)}
) : null}
{/* Right: GitHub, Theme Settings & User Menu */}
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
@@ -154,8 +154,7 @@ export function Header({ hideSidebarToggle = false, hideSearch = false }: Header
<Link to="/login">Login</Link>
</Button>
</div>
) : (
/* Authenticated User - Full menu with logout */
) : /* Authenticated User - Full menu with logout */ (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -211,11 +210,11 @@ export function Header({ hideSidebarToggle = false, hideSearch = false }: Header
</div>
{/* Mobile Search Bar (below main header) */}
{!hideSearch && (
{!hideSearch ? (
<div className="md:hidden px-4 pb-3">
<SearchBar />
</div>
)}
) : null}
</header>
);
}
+8 -8
View File
@@ -141,7 +141,7 @@ export function Sidebar({ isOpen }: SidebarProps) {
}}
>
{/* Game Navigation - Hidden when viewing shared playlist without guest access */}
{!isViewingSharedPlaylistWithoutGuestAccess && (
{!isViewingSharedPlaylistWithoutGuestAccess ? (
<div className="space-y-1">
{gameNavItems.map((item) => (
<SidebarItem
@@ -152,10 +152,10 @@ export function Sidebar({ isOpen }: SidebarProps) {
/>
))}
</div>
)}
) : null}
{/* Library Navigation - Hidden for guests and when no library features are enabled */}
{!isGuest && libraryNavItems.length > 0 && (
{!isGuest && libraryNavItems.length > 0 ? (
<SidebarSection collapsed={effectiveCollapsed}>
{libraryNavItems.map((item) => (
<SidebarItem
@@ -166,7 +166,7 @@ export function Sidebar({ isOpen }: SidebarProps) {
/>
))}
</SidebarSection>
)}
) : null}
{/* Admin/Management Section */}
<RoleGuard permissions={['users.read', 'roles.read', 'activities.read']}>
@@ -185,7 +185,7 @@ export function Sidebar({ isOpen }: SidebarProps) {
</ScrollArea>
{/* Dashboard & Settings at Bottom - Hidden for guests */}
{!isGuest && (
{!isGuest ? (
<div
className="border-t space-y-1"
style={{
@@ -193,7 +193,7 @@ export function Sidebar({ isOpen }: SidebarProps) {
transition: isMobile ? undefined : 'padding 500ms ease-out',
}}
>
{bottomNavItems.map((item) => (
{bottomNavItems.map((item) =>
item.permission ? (
<RoleGuard key={item.path} permission={item.permission}>
<SidebarItem {...item} collapsed={effectiveCollapsed} onClick={handleNavItemClick} />
@@ -201,9 +201,9 @@ export function Sidebar({ isOpen }: SidebarProps) {
) : (
<SidebarItem key={item.path} {...item} collapsed={effectiveCollapsed} onClick={handleNavItemClick} />
)
))}
)}
</div>
)}
) : null}
</aside>
</>
);
@@ -16,9 +16,9 @@ export function SidebarSection({
}: SidebarSectionProps) {
return (
<>
{showSeparator && <Separator className="my-3" />}
{showSeparator ? <Separator className="my-3" /> : null}
<div className="space-y-1">
{title && (
{title ? (
<div
className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider overflow-hidden"
style={{
@@ -31,7 +31,7 @@ export function SidebarSection({
>
{title}
</div>
)}
) : null}
{children}
</div>
</>
@@ -300,13 +300,11 @@ export function GameBrowseLayout({
<h1 className="text-3xl font-bold">{pageTitle}</h1>
{isLoading ? (
<div className="h-5 bg-muted rounded w-32 mt-1 animate-pulse"></div>
) : (
data && (
<p className="text-sm text-muted-foreground mt-1">
Found {data.total.toLocaleString()} games
</p>
)
)}
) : data ? (
<p className="text-sm text-muted-foreground mt-1">
Found {data.total.toLocaleString()} games
</p>
) : null}
{headerContent}
</div>
@@ -327,19 +325,19 @@ export function GameBrowseLayout({
</div>
{/* Active Filter Chips */}
{filterChips.length > 0 && (
{filterChips.length > 0 ? (
<FilterChips
chips={filterChips}
onRemove={handleRemoveChip}
onClearAll={handleClearAllFilters}
/>
)}
) : null}
{error && (
{error ? (
<div className="bg-red-900/20 border border-red-500 rounded-lg p-4 text-red-300">
Error loading games: {error.message}
</div>
)}
) : null}
{isLoading ? (
viewMode === 'grid' ? (
@@ -363,7 +361,7 @@ export function GameBrowseLayout({
/>
)}
{data.totalPages > 1 && (
{data.totalPages > 1 ? (
<PaginationWithInfo
currentPage={data.page}
totalPages={data.totalPages}
@@ -375,7 +373,7 @@ export function GameBrowseLayout({
showInfo={true}
className="mt-8 pb-2"
/>
)}
) : null}
</>
) : null}
</div>
+16 -16
View File
@@ -76,9 +76,9 @@ const GameCardComponent = function GameCard({
{imageUrl && !imageError ? (
<>
{/* Blur placeholder while loading */}
{!imageLoaded && (
{!imageLoaded ? (
<div className="absolute inset-0 bg-gradient-to-br from-muted to-accent animate-pulse z-0" />
)}
) : null}
{/* Game Image with hover scale effect */}
<img
@@ -105,7 +105,7 @@ const GameCardComponent = function GameCard({
{/* Glassmorphism Action Bar - slides up on hover (desktop), always visible on mobile */}
<div className="absolute bottom-0 left-0 right-0 z-20 backdrop-blur-lg bg-black/30 border-t border-white/10 flex items-center justify-start gap-1.5 px-3 py-2 translate-y-0 md:translate-y-full md:group-hover:translate-y-0 motion-safe:transition-all motion-safe:duration-300 motion-safe:ease-out">
{/* Favorite Button - Show when not favorited */}
{showFavoriteButton && !isFavorited && (
{showFavoriteButton && !isFavorited ? (
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -125,10 +125,10 @@ const GameCardComponent = function GameCard({
</TooltipTrigger>
<TooltipContent>Add to Favorites</TooltipContent>
</Tooltip>
)}
) : null}
{/* Remove from Favorites Button - Show when favorited */}
{showRemoveButton && isFavorited && (
{showRemoveButton && isFavorited ? (
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -146,10 +146,10 @@ const GameCardComponent = function GameCard({
</TooltipTrigger>
<TooltipContent>Remove from Favorites</TooltipContent>
</Tooltip>
)}
) : null}
{/* Add to Playlist Button */}
{showAddToPlaylistButton && (
{showAddToPlaylistButton ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -172,10 +172,10 @@ const GameCardComponent = function GameCard({
</TooltipTrigger>
<TooltipContent>Add to Playlist</TooltipContent>
</Tooltip>
)}
) : null}
{/* Play Button */}
{isPlayable && (
{isPlayable ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -195,7 +195,7 @@ const GameCardComponent = function GameCard({
</TooltipTrigger>
<TooltipContent>Play Game</TooltipContent>
</Tooltip>
)}
) : null}
</div>
</CardContent>
@@ -206,7 +206,7 @@ const GameCardComponent = function GameCard({
{/* Platform Badge and Favorite Indicator Row - Fixed height */}
<div className="flex items-center justify-between w-full h-6 min-h-[24px]">
{/* Platform Badge */}
{game.platformName && (
{game.platformName ? (
<Badge
className="backdrop-blur-md bg-primary/10 border border-primary/20 h-6 px-2 text-xs font-normal text-primary"
variant="outline"
@@ -221,10 +221,10 @@ const GameCardComponent = function GameCard({
/>
<span className="truncate">{game.platformName}</span>
</Badge>
)}
) : null}
{/* Favorite Indicator (non-interactive) */}
{showFavoriteIndicator && isFavorited && !isFavoritePage && (
{showFavoriteIndicator && isFavorited && !isFavoritePage ? (
<div
className="flex items-center gap-1 text-red-500"
onClick={(e) => e.stopPropagation()}
@@ -232,7 +232,7 @@ const GameCardComponent = function GameCard({
<Heart size={16} fill="currentColor" aria-hidden="true" />
{/* <span className="text-xs font-medium">Favorited</span> */}
</div>
)}
) : null}
</div>
{/* Title - Fixed 2-line height */}
@@ -245,11 +245,11 @@ const GameCardComponent = function GameCard({
{/* Developer name - Fixed height container */}
<div className="h-5 min-h-[20px] w-full">
{game.developer && (
{game.developer ? (
<p className="text-sm text-muted-foreground truncate w-full" title={game.developer}>
by {game.developer}
</p>
)}
) : null}
</div>
</CardFooter>
@@ -98,34 +98,34 @@ export function GameListItem({ game, showFavoriteButton = true, showRemoveButton
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{game.developer && (
{game.developer ? (
<span className="truncate" title={game.developer}>
{game.developer}
</span>
)}
) : null}
</div>
</div>
{/* Action Buttons */}
<div className="flex-shrink-0 flex items-center gap-2">
{/* Favorite Indicator (non-clickable, always visible if favorited) */}
{showFavoriteIndicator && isFavorited && !isFavoritePage && (
{showFavoriteIndicator && isFavorited && !isFavoritePage ? (
<div className="h-9 w-9 flex items-center justify-center">
<Heart size={18} fill="currentColor" className="text-primary" />
</div>
)}
) : null}
{/* Remove from Favorites Button (Favorites page only) */}
{showRemoveButton && isFavoritePage && (
{showRemoveButton && isFavoritePage ? (
<RemoveFavoriteButton
gameId={game.id}
size="sm"
className="h-9 w-9 p-0"
/>
)}
) : null}
{/* Add to Favorites Button (other pages, when not favorited) */}
{showFavoriteButton && !isFavoritePage && !isFavorited && (
{showFavoriteButton && !isFavoritePage && !isFavorited ? (
<FavoriteButton
gameId={game.id}
isFavorited={false}
@@ -134,9 +134,9 @@ export function GameListItem({ game, showFavoriteButton = true, showRemoveButton
className="h-9 w-9 p-0"
showOnHoverOnly={false}
/>
)}
) : null}
{showAddToPlaylistButton && (
{showAddToPlaylistButton ? (
<Button
size="sm"
variant="ghost"
@@ -155,9 +155,9 @@ export function GameListItem({ game, showFavoriteButton = true, showRemoveButton
>
<ListPlus size={18} />
</Button>
)}
) : null}
{isPlayable && (
{isPlayable ? (
<Button
size="sm"
asChild
@@ -170,7 +170,7 @@ export function GameListItem({ game, showFavoriteButton = true, showRemoveButton
<Play size={18} />
</Link>
</Button>
)}
) : null}
</div>
</div>
</CardContent>
@@ -132,7 +132,7 @@ export function GamePlayer({
return (
<div className={`${containerClasses} ${isAspectVideo ? 'flex flex-col' : ''}`}>
{/* Player Controls */}
{showControls && (
{showControls ? (
<div className="flex items-center justify-between bg-card/90 backdrop-blur-sm px-4 py-3 flex-shrink-0 border-b border-border">
<div className="flex items-center gap-3">
<Play size={18} className="text-primary" />
@@ -145,7 +145,7 @@ export function GamePlayer({
</span>
</div>
</div>
{allowFullscreen && (
{allowFullscreen ? (
<button
onClick={toggleFullscreen}
className="flex items-center gap-2 px-4 py-2 bg-secondary hover:bg-secondary/80 rounded-lg transition-colors"
@@ -163,9 +163,9 @@ export function GamePlayer({
</>
)}
</button>
)}
) : null}
</div>
)}
) : null}
{/* Game Player Container */}
<div
@@ -208,7 +208,7 @@ export function GamePlayer({
setIframeError('Failed to load HTML5 game');
}}
/>
{iframeError && (
{iframeError ? (
<div className="absolute inset-0 flex items-center justify-center bg-background/90 z-10">
<div className="text-center max-w-md p-6">
<AlertCircle size={48} className="text-red-500 mx-auto mb-4" />
@@ -224,7 +224,7 @@ export function GamePlayer({
</p>
</div>
</div>
)}
) : null}
</>
) : (
<div className="flex items-center justify-center h-full">
@@ -302,7 +302,7 @@ export function RufflePlayer({
className="ruffle-player-wrapper"
/>
{isLoading && (
{isLoading ? (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto mb-4"></div>
@@ -310,9 +310,9 @@ export function RufflePlayer({
<p className="text-xs text-gray-400 mt-2">Powered by Ruffle</p>
</div>
</div>
)}
) : null}
{error && (
{error ? (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 z-10">
<div className="text-center max-w-md p-6">
<div className="text-red-500 text-5xl mb-4">⚠️</div>
@@ -331,7 +331,7 @@ export function RufflePlayer({
</p>
</div>
</div>
)}
) : null}
</div>
);
}
@@ -154,11 +154,11 @@ export function AddToPlaylistModal({
<p className="font-medium text-sm truncate">
{playlist.title}
</p>
{playlist.description && (
{playlist.description ? (
<p className="text-xs text-muted-foreground line-clamp-1">
{playlist.description}
</p>
)}
) : null}
<p className="text-xs text-muted-foreground mt-0.5">
{playlist.gameCount} {playlist.gameCount === 1 ? 'game' : 'games'}
</p>
@@ -165,15 +165,15 @@ export function BrowseCommunityPlaylistsModal({ isOpen, onClose }: BrowseCommuni
{/* Content */}
<div className="flex-1 overflow-y-auto space-y-3">
{/* Loading State */}
{isLoading && (
{isLoading ? (
<div className="flex flex-col items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mb-4"></div>
<p className="text-gray-400">Loading community playlists...</p>
</div>
)}
) : null}
{/* Error State */}
{error && (
{error ? (
<div className="text-center py-12">
<p className="text-red-400 mb-4">Failed to load community playlists</p>
<Button
@@ -183,19 +183,19 @@ export function BrowseCommunityPlaylistsModal({ isOpen, onClose }: BrowseCommuni
Try Again
</Button>
</div>
)}
) : null}
{/* Empty State */}
{!isLoading && !error && filteredPlaylists.length === 0 && (
{!isLoading && !error && filteredPlaylists.length === 0 ? (
<div className="text-center py-12 text-gray-400">
{searchQuery || selectedCategory !== 'all'
? 'No playlists match your search'
: 'No community playlists available'}
</div>
)}
) : null}
{/* Playlist Cards */}
{!isLoading && !error && filteredPlaylists.map((playlist, index) => (
{!isLoading && !error ? filteredPlaylists.map((playlist, index) => (
<div
key={`${playlist.downloadUrl}-${index}`}
className="bg-gray-700 rounded-lg p-4 hover:bg-gray-650 transition-colors"
@@ -206,11 +206,11 @@ export function BrowseCommunityPlaylistsModal({ isOpen, onClose }: BrowseCommuni
<p className="text-sm text-gray-400 mb-2">
by {playlist.author} • {playlist.category}
</p>
{playlist.description && (
{playlist.description ? (
<p className="text-sm text-gray-300 line-clamp-2">
{playlist.description}
</p>
)}
) : null}
</div>
<Button
onClick={() => handleDownload(playlist)}
@@ -226,18 +226,18 @@ export function BrowseCommunityPlaylistsModal({ isOpen, onClose }: BrowseCommuni
</Button>
</div>
</div>
))}
)) : null}
</div>
{/* Footer Info */}
{!isLoading && !error && filteredPlaylists.length > 0 && (
{!isLoading && !error && filteredPlaylists.length > 0 ? (
<div className="mt-4 pt-4 border-t border-gray-700">
<p className="text-sm text-gray-400 text-center">
Showing {filteredPlaylists.length} playlist{filteredPlaylists.length !== 1 ? 's' : ''}
{selectedCategory !== 'all' && ` in ${selectedCategory}`}
</p>
</div>
)}
) : null}
</DialogBody>
</DialogContent>
</Dialog>
@@ -88,12 +88,12 @@ export function IconSelector({
return (
<div className="space-y-2">
{label && (
{label ? (
<Label className="flex items-center gap-2 text-sm font-medium">
<Sparkles className="h-3.5 w-3.5 text-primary" />
{label}
</Label>
)}
) : null}
<Popover open={open} onOpenChange={handleOpenChange} modal={true}>
<PopoverTrigger asChild>
@@ -171,7 +171,7 @@ export function IconSelector({
</div>
{/* Footer */}
{value && (
{value ? (
<div className="border-t bg-muted/30 p-3">
<Button
variant="ghost"
@@ -187,7 +187,7 @@ export function IconSelector({
Clear Selection
</Button>
</div>
)}
) : null}
</PopoverContent>
</Popover>
</div>
@@ -101,7 +101,7 @@ const PlaylistCardComponent = function PlaylistCard({
{/* COLUMN 3: Action Buttons */}
<div className="flex flex-col gap-2 flex-shrink-0">
{/* Menu Button (always visible) */}
{showActions && (onEdit || onDelete || onShare) && (
{showActions && (onEdit || onDelete || onShare) ? (
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e: React.MouseEvent) => e.preventDefault()}>
<Button
@@ -114,19 +114,19 @@ const PlaylistCardComponent = function PlaylistCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{onEdit && (
{onEdit ? (
<DropdownMenuItem onClick={handleEdit}>
<Edit size={16} className="mr-2" />
Edit
</DropdownMenuItem>
)}
{onShare && (
) : null}
{onShare ? (
<DropdownMenuItem onClick={handleShare}>
<Share2 size={16} className="mr-2" />
Share
</DropdownMenuItem>
)}
{onDelete && (
) : null}
{onDelete ? (
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive focus:text-destructive"
@@ -134,10 +134,10 @@ const PlaylistCardComponent = function PlaylistCard({
<Trash2 size={16} className="mr-2" />
Delete
</DropdownMenuItem>
)}
) : null}
</DropdownMenuContent>
</DropdownMenu>
)}
) : null}
{/* Share Indicator Button (icon only) */}
{playlist.isPublic && playlist.shareToken ? (
@@ -42,11 +42,7 @@ interface SharePlaylistDialogProps {
playlist: UserPlaylist;
}
export function SharePlaylistDialog({
isOpen,
onClose,
playlist,
}: SharePlaylistDialogProps) {
export function SharePlaylistDialog({ isOpen, onClose, playlist }: SharePlaylistDialogProps) {
const { user } = useAuthStore();
const isAdmin = user?.permissions.includes('settings.update');
@@ -102,9 +98,7 @@ export function SharePlaylistDialog({
useEffect(() => {
if (!isOpen) {
setIsSharing(playlist.isPublic);
setExpiresAt(
playlist.shareExpiresAt ? new Date(playlist.shareExpiresAt) : undefined
);
setExpiresAt(playlist.shareExpiresAt ? new Date(playlist.shareExpiresAt) : undefined);
setShowOwner(playlist.showOwner || false);
setShareData(null);
}
@@ -214,32 +208,26 @@ export function SharePlaylistDialog({
id="enable-sharing"
checked={isSharing}
onCheckedChange={handleToggleSharing}
disabled={
enableSharingMutation.isPending ||
disableSharingMutation.isPending
}
disabled={enableSharingMutation.isPending || disableSharingMutation.isPending}
/>
</div>
{isSharing && (
{isSharing ? (
<>
{/* Warning Alert */}
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Anyone with the link can view this playlist and its games,
regardless of guest access settings.
Anyone with the link can view this playlist and its games, regardless of guest
access settings.
</AlertDescription>
</Alert>
{/* Domain Selector (admin only) */}
{isAdmin && domains && domains.length > 0 && shareData && (
{isAdmin && domains && domains.length > 0 && shareData ? (
<div className="space-y-2">
<Label htmlFor="share-domain">Domain</Label>
<Select
value={selectedDomain}
onValueChange={setSelectedDomain}
>
<Select value={selectedDomain} onValueChange={setSelectedDomain}>
<SelectTrigger id="share-domain" className="w-full">
<SelectValue placeholder="Current URL" />
</SelectTrigger>
@@ -256,10 +244,10 @@ export function SharePlaylistDialog({
</SelectContent>
</Select>
</div>
)}
) : null}
{/* Share URL */}
{shareData && (
{shareData ? (
<div className="space-y-2">
<Label htmlFor="share-url">Share URL</Label>
<div className="flex gap-2">
@@ -280,7 +268,7 @@ export function SharePlaylistDialog({
</Button>
</div>
</div>
)}
) : null}
{/* Expiry Date */}
<div className="space-y-2">
@@ -303,9 +291,7 @@ export function SharePlaylistDialog({
<Checkbox
id="show-owner"
checked={showOwner}
onCheckedChange={(checked) =>
setShowOwner(checked as boolean)
}
onCheckedChange={(checked) => setShowOwner(checked as boolean)}
/>
<div className="grid gap-1.5 leading-none">
<Label
@@ -321,7 +307,7 @@ export function SharePlaylistDialog({
</div>
{/* Update Settings Button */}
{shareData && hasSettingsChanged() && (
{shareData && hasSettingsChanged() ? (
<Button
type="button"
variant="secondary"
@@ -329,14 +315,12 @@ export function SharePlaylistDialog({
disabled={isLoadingShareData}
className="w-full"
>
{isLoadingShareData
? 'Updating...'
: 'Update Share Settings'}
{isLoadingShareData ? 'Updating...' : 'Update Share Settings'}
</Button>
)}
) : null}
{/* Regenerate Link */}
{shareData && (
{shareData ? (
<div className="space-y-2 pt-2 border-t">
<Label>Regenerate Link</Label>
<p className="text-sm text-muted-foreground mb-2">
@@ -354,14 +338,12 @@ export function SharePlaylistDialog({
regenerateTokenMutation.isPending ? 'animate-spin' : ''
}`}
/>
{regenerateTokenMutation.isPending
? 'Regenerating...'
: 'Regenerate Link'}
{regenerateTokenMutation.isPending ? 'Regenerating...' : 'Regenerate Link'}
</Button>
</div>
)}
) : null}
</>
)}
) : null}
</DialogBody>
<DialogFooter>
@@ -179,11 +179,11 @@ const IconButton = memo(function IconButton({
)}
/>
{isSelected && (
{isSelected ? (
<div className="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full bg-primary flex items-center justify-center animate-in zoom-in-50 duration-200">
<Check size={CHECK_SIZE} className="text-primary-foreground" />
</div>
)}
) : null}
</button>
);
});
@@ -113,21 +113,21 @@ export function PermissionSelector({
</DialogHeader>
<DialogBody>
{!permissions && (
{!permissions ? (
<div className="text-center py-8 text-muted-foreground">
Loading permissions...
</div>
)}
) : null}
{updatePermissionsMutation.isError && (
{updatePermissionsMutation.isError ? (
<Alert variant="destructive" className="mb-4">
<AlertDescription>
{getErrorMessage(updatePermissionsMutation.error) || "Failed to update permissions"}
</AlertDescription>
</Alert>
)}
) : null}
{permissions && (
{permissions ? (
<>
<div className="space-y-6">
{Object.entries(groupedPermissions).map(([resource, perms]) => (
@@ -166,7 +166,7 @@ export function PermissionSelector({
))}
</div>
</>
)}
) : null}
</DialogBody>
<DialogFooter className="flex flex-col sm:flex-row sm:justify-between gap-4 sm:gap-0 items-center">
+8 -8
View File
@@ -108,13 +108,13 @@ export function RoleForm({ role, onClose, onSuccess }: RoleFormProps) {
</DialogHeader>
<DialogBody>
{mutation.isError && (
{mutation.isError ? (
<Alert variant="destructive" className="mb-4">
<AlertDescription>
{getErrorMessage(mutation.error) || 'Operation failed'}
</AlertDescription>
</Alert>
)}
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
@@ -177,7 +177,7 @@ export function RoleForm({ role, onClose, onSuccess }: RoleFormProps) {
)}
/>
{!isEditMode && permissions && permissions.length > 0 && (
{!isEditMode && permissions && permissions.length > 0 ? (
<FormField
control={form.control}
name="permissionIds"
@@ -219,11 +219,11 @@ export function RoleForm({ role, onClose, onSuccess }: RoleFormProps) {
<FormLabel className="text-sm font-normal cursor-pointer">
{permission.name}
</FormLabel>
{permission.description && (
{permission.description ? (
<FormDescription className="text-xs">
{permission.description}
</FormDescription>
)}
) : null}
</div>
</FormItem>
)
@@ -235,15 +235,15 @@ export function RoleForm({ role, onClose, onSuccess }: RoleFormProps) {
</FormItem>
)}
/>
)}
) : null}
{isEditMode && (
{isEditMode ? (
<Alert className="border-blue-500 bg-blue-500/10 text-blue-400">
<AlertDescription>
Permissions can be managed separately from role details. Use the permissions management page to update role permissions.
</AlertDescription>
</Alert>
)}
) : null}
<DialogFooter>
<Button
+2 -2
View File
@@ -145,9 +145,9 @@ export function RoleTable({ onEdit, onManagePermissions }: RoleTableProps) {
</RoleGuard>
</DropdownMenuContent>
</DropdownMenu>
{isSystemRole && (
{isSystemRole ? (
<span className="text-xs text-muted-foreground">(System)</span>
)}
) : null}
</div>
);
},
@@ -49,7 +49,7 @@ export function FilterChips({ chips, onRemove, onClearAll, className }: FilterCh
</Badge>
))}
{chips.length > 1 && onClearAll && (
{chips.length > 1 && onClearAll ? (
<Button
variant="ghost"
size="sm"
@@ -58,7 +58,7 @@ export function FilterChips({ chips, onRemove, onClearAll, className }: FilterCh
>
Clear All
</Button>
)}
) : null}
</div>
);
}
@@ -68,7 +68,7 @@ export function FilterDropdown({
<div className="sticky top-0 bg-popover border-b p-2 z-10">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">{label}</span>
{selectedCount > 0 && (
{selectedCount > 0 ? (
<Button
variant="ghost"
size="sm"
@@ -78,7 +78,7 @@ export function FilterDropdown({
>
Clear All
</Button>
)}
) : null}
</div>
</div>
@@ -150,7 +150,7 @@ export function FilterPanel({ filters, showPlatformFilter = true }: FilterPanelP
{/* All Filters */}
<div className="space-y-3">
{/* With Platform Filter */}
{showPlatformFilter && (
{showPlatformFilter ? (
<>
{/* Desktop Layout: Single Row */}
<div className="hidden md:flex items-center gap-3 flex-wrap">
@@ -164,10 +164,10 @@ export function FilterPanel({ filters, showPlatformFilter = true }: FilterPanelP
{renderFilters(true)}
</div>
</>
)}
) : null}
{/* Filter Popover - Desktop and Mobile (No platform filter) */}
{!showPlatformFilter && (
{!showPlatformFilter ? (
<>
{/* Desktop Layout: Single Row */}
<div className="hidden md:flex items-center gap-3 flex-wrap">
@@ -181,7 +181,7 @@ export function FilterPanel({ filters, showPlatformFilter = true }: FilterPanelP
{renderFilters(true)}
</div>
</>
)}
) : null}
{/* Filter Badges - Data-driven rendering */}
<div className="space-y-2">
@@ -98,7 +98,7 @@ export function AppSettingsTab({ tabContentVariants }: AppSettingsTabProps) {
exit="exit"
className="space-y-6"
>
{isAdmin && appSettings && (
{isAdmin && appSettings ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center gap-2 mb-4">
<Palette size={24} className="text-primary" />
@@ -248,9 +248,9 @@ export function AppSettingsTab({ tabContentVariants }: AppSettingsTabProps) {
</div>
</div>
</div>
)}
) : null}
{isAdmin && (
{isAdmin ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center gap-2 mb-4">
<Globe size={24} className="text-primary" />
@@ -280,9 +280,9 @@ export function AppSettingsTab({ tabContentVariants }: AppSettingsTabProps) {
placeholder="e.g., play.example.com"
disabled={addDomainMutation.isPending}
/>
{domainError && (
{domainError ? (
<p className="text-xs text-destructive mt-1">{domainError}</p>
)}
) : null}
</div>
<Button
onClick={handleAddDomain}
@@ -314,11 +314,11 @@ export function AppSettingsTab({ tabContentVariants }: AppSettingsTabProps) {
<span className="text-sm font-mono truncate">
{domain.hostname}
</span>
{domain.isDefault && (
{domain.isDefault ? (
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full whitespace-nowrap">
default
</span>
)}
) : null}
</label>
<Button
variant="ghost"
@@ -340,7 +340,7 @@ export function AppSettingsTab({ tabContentVariants }: AppSettingsTabProps) {
</p>
)}
</div>
)}
) : null}
</motion.div>
);
@@ -61,7 +61,7 @@ export function FeaturesSettingsTab({
exit="exit"
className="space-y-6"
>
{isAdmin && featureSettings && (
{isAdmin && featureSettings ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center gap-2 mb-4">
<ToggleLeft size={24} className="text-primary" />
@@ -146,7 +146,7 @@ export function FeaturesSettingsTab({
</div>
</div>
</div>
)}
) : null}
</motion.div>
);
}
@@ -124,7 +124,7 @@ export function GeneralSettingsTab({
</div>
{/* Date & Time Format Settings - Available to all authenticated users */}
{userSettings && (
{userSettings ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center gap-2 mb-4">
<Calendar size={24} className="text-primary" />
@@ -187,17 +187,17 @@ export function GeneralSettingsTab({
</div>
</div>
</div>
)}
) : null}
{/* Authentication Settings (Admin Only) */}
{isAdmin && (
{isAdmin ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center gap-2 mb-4">
<Shield size={24} className="text-primary" />
<h2 className="text-xl font-semibold">Authentication Settings</h2>
</div>
{authSettings && (
{authSettings ? (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
@@ -244,9 +244,9 @@ export function GeneralSettingsTab({
/>
</div>
</div>
)}
) : null}
</div>
)}
) : null}
</motion.div>
);
}
@@ -304,14 +304,14 @@ export function UpdateSettingsTab({
className="space-y-6"
>
{/* Game Metadata Updates Section */}
{isAdmin && (
{isAdmin ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Database size={24} className="text-primary" />
<h2 className="text-xl font-semibold">Game Metadata</h2>
</div>
{metadataInfo?.edition !== "ultimate" && (
{metadataInfo?.edition !== "ultimate" ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -334,11 +334,11 @@ export function UpdateSettingsTab({
</p>
</TooltipContent>
</Tooltip>
)}
) : null}
</div>
{/* Ultimate Edition: No metadata sync available */}
{metadataInfo?.edition === "ultimate" && (
{metadataInfo?.edition === "ultimate" ? (
<div className="bg-muted border border-border rounded-lg p-4 flex items-center gap-3">
<Info size={20} className="text-muted-foreground flex-shrink-0" />
<div>
@@ -351,20 +351,20 @@ export function UpdateSettingsTab({
</p>
</div>
</div>
)}
) : null}
{/* Infinity Edition: Metadata sync UI */}
{metadataInfo?.edition !== "ultimate" && (
{metadataInfo?.edition !== "ultimate" ? (
<>
{/* Error Message */}
{error && (
{error ? (
<div className="bg-red-500/10 border border-red-500 rounded-lg p-3 mb-4 flex items-start gap-2">
<p className="text-foreground text-sm font-medium">{error}</p>
</div>
)}
) : null}
{/* Skeleton Loading State */}
{!metadataInfo && (
{!metadataInfo ? (
<div className="bg-muted border border-border rounded-lg p-4 animate-pulse">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
@@ -378,10 +378,10 @@ export function UpdateSettingsTab({
<div className="w-24 h-10 bg-accent rounded-lg flex-shrink-0"></div>
</div>
</div>
)}
) : null}
{/* Metadata Update Info */}
{metadataInfo && (
{metadataInfo ? (
<div>
{metadataInfo.gamesUpdateAvailable ? (
<div className="bg-primary/10 border border-primary rounded-lg p-4">
@@ -416,15 +416,15 @@ export function UpdateSettingsTab({
<>Game metadata updates are available</>
)}
</p>
{metadataInfo.lastCheckedTime && (
{metadataInfo.lastCheckedTime ? (
<p className="text-muted-foreground text-xs mt-2">
Last synced:{" "}
<FormattedDate date={metadataInfo.lastCheckedTime} type="datetime" />
</p>
)}
) : null}
{/* Progress Bar */}
{isSyncingMetadata && (
{isSyncingMetadata ? (
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-primary">
@@ -443,7 +443,7 @@ export function UpdateSettingsTab({
/>
</div>
</div>
)}
) : null}
</div>
</div>
<Tooltip>
@@ -474,24 +474,24 @@ export function UpdateSettingsTab({
<p className="text-foreground font-medium">
Game metadata is up to date
</p>
{metadataInfo.lastCheckedTime && (
{metadataInfo.lastCheckedTime ? (
<p className="text-muted-foreground text-xs mt-1">
Last synced:{" "}
<FormattedDate date={metadataInfo.lastCheckedTime} type="datetime" />
</p>
)}
) : null}
</div>
</div>
)}
</div>
)}
) : null}
</>
)}
) : null}
</div>
)}
) : null}
{/* Ruffle Emulator Management (Admin Only) */}
{isAdmin && ruffleVersion && (
{isAdmin && ruffleVersion ? (
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
<div className="flex items-center gap-2 mb-4">
<Download size={24} className="text-primary" />
@@ -537,7 +537,7 @@ export function UpdateSettingsTab({
</Button>
</div>
{updateCheckResult && (
{updateCheckResult ? (
<div className="p-4 border rounded-lg bg-muted/50 space-y-3">
<div className="flex items-center justify-between">
<div className="flex-1">
@@ -551,7 +551,7 @@ export function UpdateSettingsTab({
? "A new version of Ruffle is available for download."
: "You have the latest version of Ruffle installed."}
</p>
{updateCheckResult.publishedAt && (
{updateCheckResult.publishedAt ? (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
<span>
@@ -562,9 +562,9 @@ export function UpdateSettingsTab({
/>
</span>
</div>
)}
) : null}
</div>
{updateCheckResult.updateAvailable && (
{updateCheckResult.updateAvailable ? (
<Button
onClick={() => updateRuffle.mutate()}
disabled={updateRuffle.isPending}
@@ -581,12 +581,12 @@ export function UpdateSettingsTab({
</>
)}
</Button>
)}
) : null}
</div>
{/* Changelog Section - Only show if update is available */}
{updateCheckResult.updateAvailable &&
updateCheckResult.changelog && (
updateCheckResult.changelog ? (
<div className="border-t pt-3">
<button
onClick={() => setShowChangelog(!showChangelog)}
@@ -599,7 +599,7 @@ export function UpdateSettingsTab({
)}
<span>{showChangelog ? "Hide" : "View"} Changelog</span>
</button>
{showChangelog && (
{showChangelog ? (
<div className="mt-3 p-3 bg-background border rounded-lg">
<div className="text-xs text-muted-foreground mb-2 font-medium">
Release Notes:
@@ -610,11 +610,11 @@ export function UpdateSettingsTab({
</ReactMarkdown>
</div>
</div>
)}
) : null}
</div>
)}
) : null}
</div>
)}
) : null}
<div className="text-sm text-muted-foreground pt-2 space-y-1">
<p>
@@ -629,7 +629,7 @@ export function UpdateSettingsTab({
</div>
</div>
</div>
)}
) : null}
</motion.div>
);
}
@@ -44,11 +44,11 @@ export class ChartErrorBoundary extends Component<Props, State> {
<p className="text-muted-foreground text-sm">
Unable to render chart. Please try refreshing the page.
</p>
{this.state.error && (
{this.state.error ? (
<p className="text-xs text-muted-foreground mt-2 font-mono">
{this.state.error.message}
</p>
)}
) : null}
</div>
</div>
</div>
@@ -89,9 +89,9 @@ export function ThemePicker() {
title={colorPalette[color].label}
aria-label={`Select ${colorPalette[color].label} theme`}
>
{isSelected && (
{isSelected ? (
<Check className="absolute inset-0 m-auto h-5 w-5 text-white drop-shadow-lg" />
)}
) : null}
</button>
);
})}
+2 -2
View File
@@ -191,7 +191,7 @@ export function DataTable<TData, TValue>({
</Table>
</div>
{pagination && pagination.totalPages > 1 && (
{pagination && pagination.totalPages > 1 ? (
<div className="flex flex-col sm:flex-row gap-4 sm:justify-between sm:items-center p-4 border-t bg-muted/30">
<div className="text-sm text-muted-foreground">
Page {pagination.page} of {pagination.totalPages} ({pagination.total} total)
@@ -237,7 +237,7 @@ export function DataTable<TData, TValue>({
</Button>
</div>
</div>
)}
) : null}
</Card>
</div>
)
+4 -4
View File
@@ -74,9 +74,9 @@ export function DatePicker({
>
<CalendarIcon className="mr-2 h-4 w-4" />
{displayDate || placeholder}
{required && !date && (
{required && !date ? (
<span className="ml-1 text-destructive">*</span>
)}
) : null}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
@@ -90,7 +90,7 @@ export function DatePicker({
</PopoverContent>
</Popover>
{clearable && (
{clearable ? (
<Button
variant="outline"
size="icon"
@@ -101,7 +101,7 @@ export function DatePicker({
>
<X className="h-4 w-4" />
</Button>
)}
) : null}
</div>
);
}
+6 -6
View File
@@ -85,7 +85,7 @@ export function Pagination({
className={cn('flex items-center justify-center gap-1', className)}
>
{/* First page button */}
{showFirstLast && (
{showFirstLast ? (
<Button
variant="outline"
size="icon"
@@ -96,7 +96,7 @@ export function Pagination({
>
<ChevronsLeft size={16} />
</Button>
)}
) : null}
{/* Previous page button */}
<Button
@@ -160,7 +160,7 @@ export function Pagination({
</Button>
{/* Last page button */}
{showFirstLast && (
{showFirstLast ? (
<Button
variant="outline"
size="icon"
@@ -171,7 +171,7 @@ export function Pagination({
>
<ChevronsRight size={16} />
</Button>
)}
) : null}
</nav>
);
}
@@ -234,13 +234,13 @@ export function PaginationWithInfo({
}: PaginationWithInfoProps) {
return (
<div className={cn('flex flex-col items-center gap-4', className)}>
{showInfo && (
{showInfo ? (
<PaginationInfo
currentPage={currentPage}
pageSize={pageSize}
totalItems={totalItems}
/>
)}
) : null}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
+2 -2
View File
@@ -28,12 +28,12 @@ export function PlatformIcon({ platformName, size = 20, className = '' }: Platfo
className="relative inline-flex items-center justify-center flex-shrink-0"
style={{ width: size, height: size }}
>
{imageLoading && (
{imageLoading ? (
<div
className="absolute inset-0 bg-muted rounded animate-pulse"
style={{ width: size, height: size }}
/>
)}
) : null}
<img
src={logoUrl}
alt={`${platformName} logo`}
+3 -3
View File
@@ -19,10 +19,10 @@ export function Toaster() {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
{title ? <ToastTitle>{title}</ToastTitle> : null}
{description ? (
<ToastDescription>{description}</ToastDescription>
)}
) : null}
</div>
{action}
<ToastClose />
@@ -80,13 +80,13 @@ export function ChangePasswordDialog({ isOpen, user, onClose, onSuccess }: Chang
</DialogHeader>
<DialogBody>
{changePasswordMutation.isError && (
{changePasswordMutation.isError ? (
<Alert variant="destructive" className="mb-4">
<AlertDescription>
{getErrorMessage(changePasswordMutation.error) || 'Failed to change password'}
</AlertDescription>
</Alert>
)}
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+8 -8
View File
@@ -120,17 +120,17 @@ export function UserForm({ user, onClose, onSuccess }: UserFormProps) {
</DialogHeader>
<DialogBody>
{mutation.isError && (
{mutation.isError ? (
<Alert variant="destructive" className="mb-4">
<AlertDescription>
{getErrorMessage(mutation.error) || 'Operation failed'}
</AlertDescription>
</Alert>
)}
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{!isEditMode && (
{!isEditMode ? (
<FormField
control={form.control}
name="username"
@@ -147,9 +147,9 @@ export function UserForm({ user, onClose, onSuccess }: UserFormProps) {
</FormItem>
)}
/>
)}
) : null}
{isEditMode && user && (
{isEditMode && user ? (
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Username
@@ -161,7 +161,7 @@ export function UserForm({ user, onClose, onSuccess }: UserFormProps) {
/>
<p className="text-xs text-muted-foreground mt-1">Username cannot be changed</p>
</div>
)}
) : null}
<FormField
control={form.control}
@@ -181,7 +181,7 @@ export function UserForm({ user, onClose, onSuccess }: UserFormProps) {
)}
/>
{!isEditMode && (
{!isEditMode ? (
<FormField
control={form.control}
name="password"
@@ -199,7 +199,7 @@ export function UserForm({ user, onClose, onSuccess }: UserFormProps) {
</FormItem>
)}
/>
)}
) : null}
<FormField
control={form.control}
+2 -2
View File
@@ -68,7 +68,7 @@ export function DialogProvider({ children }: { children: ReactNode }) {
return (
<DialogContext.Provider value={{ showToast, showConfirm }}>
{children}
{confirmState && (
{confirmState ? (
<ConfirmDialog
isOpen={confirmState.isOpen}
title={confirmState.options.title}
@@ -79,7 +79,7 @@ export function DialogProvider({ children }: { children: ReactNode }) {
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
) : null}
</DialogContext.Provider>
);
}
+2 -2
View File
@@ -29,11 +29,11 @@ export function ActivitiesView() {
<ActivityHeader />
{/* Dashboard - only render if statistics feature is enabled */}
{enableStatistics && (
{enableStatistics ? (
<div className="mb-6">
<ActivityDashboard />
</div>
)}
) : null}
{/* Activity Table */}
<ActivityTable />
+2 -2
View File
@@ -73,7 +73,7 @@ export function FavoritesView() {
{/* Controls */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
{/* Sort Dropdown */}
{games.length > 0 && (
{games.length > 0 ? (
<div className="flex items-center gap-2 w-full sm:w-auto">
<Label htmlFor="sort-select" className="text-sm whitespace-nowrap hidden sm:inline">
Sort by:
@@ -110,7 +110,7 @@ export function FavoritesView() {
</SelectContent>
</Select>
</div>
)}
) : null}
{/* View Mode Controls */}
<CardSizeControl />
+24 -24
View File
@@ -210,13 +210,13 @@ export function GameDetailView() {
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1">
{/* Game Logo */}
{logoUrl && !logoError && (
{logoUrl && !logoError ? (
<div className="flex-shrink-0 w-20 h-20 bg-muted rounded-lg overflow-hidden flex items-center justify-center p-2 relative">
{logoLoading && (
{logoLoading ? (
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm">
<Loader2 size={20} className="text-muted-foreground animate-spin" />
</div>
)}
) : null}
<img
src={logoUrl}
alt={`${game.title} logo`}
@@ -230,31 +230,31 @@ export function GameDetailView() {
}}
/>
</div>
)}
) : null}
<div className="flex-1 min-w-0">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold mb-2 break-words">{game.title}</h1>
{game.alternateTitles && (
{game.alternateTitles ? (
<p className="text-muted-foreground text-sm break-words">Also known as: {game.alternateTitles}</p>
)}
) : null}
{/* Play Statistics - Under title */}
{(currentGameStats?.lastPlayedAt || currentGameStats?.totalPlaytimeSeconds) && (
{(currentGameStats?.lastPlayedAt || currentGameStats?.totalPlaytimeSeconds) ? (
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
{currentGameStats?.lastPlayedAt && (
{currentGameStats?.lastPlayedAt ? (
<div className="flex items-center gap-1.5">
<Clock size={14} />
<span>Last played {formatRelativeTime(currentGameStats.lastPlayedAt)}</span>
</div>
)}
{currentGameStats?.totalPlaytimeSeconds && (
) : null}
{currentGameStats?.totalPlaytimeSeconds ? (
<div className="flex items-center gap-1.5">
<span>•</span>
<span>{formatDuration(currentGameStats.totalPlaytimeSeconds)} played</span>
</div>
)}
) : null}
</div>
)}
) : null}
</div>
</div>
@@ -289,9 +289,9 @@ export function GameDetailView() {
)}
{/* Error Display */}
{downloadError && (
{downloadError ? (
<p className="text-sm text-red-500 font-medium">{downloadError}</p>
)}
) : null}
</>
) : (
<Badge variant="secondary" className="px-4 py-2 text-sm sm:text-base font-medium flex items-center gap-2">
@@ -303,16 +303,16 @@ export function GameDetailView() {
</div>
{/* Game Screenshot */}
{screenshotUrl && (
{screenshotUrl ? (
<div className="aspect-video bg-muted rounded-lg overflow-hidden flex items-center justify-center relative">
{imageLoading && !imageError && (
{imageLoading && !imageError ? (
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm">
<div className="flex flex-col items-center gap-3">
<Loader2 size={48} className="text-muted-foreground animate-spin" />
<span className="text-sm text-muted-foreground">Loading screenshot...</span>
</div>
</div>
)}
) : null}
{!imageError ? (
<img
src={screenshotUrl}
@@ -333,18 +333,18 @@ export function GameDetailView() {
</div>
)}
</div>
)}
) : null}
<GameInfoGrid game={game} />
{game.originalDescription && (
{game.originalDescription ? (
<div>
<h2 className="text-lg font-semibold mb-2">Description</h2>
<p className="leading-relaxed">{game.originalDescription}</p>
</div>
)}
) : null}
{game.tagsStr && (
{game.tagsStr ? (
<div>
<h2 className="text-lg font-semibold mb-2">Tags</h2>
<div className="flex flex-wrap gap-2">
@@ -353,14 +353,14 @@ export function GameDetailView() {
))}
</div>
</div>
)}
) : null}
{game.notes && (
{game.notes ? (
<div className="bg-muted/50 rounded p-4">
<h3 className="text-sm font-semibold text-muted-foreground mb-1">Notes</h3>
<p className="text-sm">{game.notes}</p>
</div>
)}
) : null}
</div>
</div>
</ErrorBoundary>
+18 -18
View File
@@ -88,7 +88,7 @@ export function GamePlayerView() {
return (
<div className={isFullscreen ? 'fixed inset-0 z-50 bg-black' : 'max-w-6xl mx-auto space-y-6'}>
{/* Back button - only shown in normal mode */}
{!isFullscreen && (
{!isFullscreen ? (
<button
onClick={() => navigate(-1)}
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
@@ -96,22 +96,22 @@ export function GamePlayerView() {
<ArrowLeft size={20} />
Back
</button>
)}
) : null}
{/* Game Player Card - adjusts styling based on fullscreen */}
<div className={isFullscreen ? 'w-full h-full' : 'bg-card rounded-lg overflow-hidden shadow-xl border border-border'}>
{/* Game Header - only shown in normal mode */}
{!isFullscreen && (
{!isFullscreen ? (
<div className="px-6 py-4 border-b border-border">
<div className="flex items-start gap-4">
{/* Game Logo */}
{getGameLogoUrl(game.id) && !logoError && (
{getGameLogoUrl(game.id) && !logoError ? (
<div className="flex-shrink-0 w-16 h-16 bg-muted rounded-lg overflow-hidden flex items-center justify-center p-1.5 relative">
{logoLoading && (
{logoLoading ? (
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm">
<Loader2 size={16} className="text-muted-foreground animate-spin" />
</div>
)}
) : null}
<img
src={getGameLogoUrl(game.id)}
alt={`${game.title} logo`}
@@ -123,17 +123,17 @@ export function GamePlayerView() {
}}
/>
</div>
)}
) : null}
<div className="flex-1">
<h1 className="text-2xl font-bold mb-1">{game.title}</h1>
{game.developer && (
{game.developer ? (
<p className="text-muted-foreground text-sm">by {game.developer}</p>
)}
) : null}
</div>
</div>
</div>
)}
) : null}
{/* Game Player Container - ALWAYS VISIBLE, persists in both modes */}
<div className={isFullscreen ? 'w-full h-screen' : 'aspect-video bg-black'}>
@@ -150,21 +150,21 @@ export function GamePlayerView() {
</div>
{/* Game Info Below Player - only shown in normal mode */}
{!isFullscreen && (
{!isFullscreen ? (
<div className="px-6 py-6 space-y-6">
{/* Game Info Grid */}
<GameInfoGrid game={game} launchData={launchData} />
{/* Description */}
{game.originalDescription && (
{game.originalDescription ? (
<div>
<h2 className="text-lg font-semibold mb-2">Description</h2>
<p className="leading-relaxed">{game.originalDescription}</p>
</div>
)}
) : null}
{/* Tags */}
{game.tagsStr && (
{game.tagsStr ? (
<div>
<h2 className="text-lg font-semibold mb-2">Tags</h2>
<div className="flex flex-wrap gap-2">
@@ -173,17 +173,17 @@ export function GamePlayerView() {
))}
</div>
</div>
)}
) : null}
{/* Notes */}
{game.notes && (
{game.notes ? (
<div className="bg-muted/50 rounded p-4">
<h3 className="text-sm font-semibold text-muted-foreground mb-1">Notes</h3>
<p className="text-sm">{game.notes}</p>
</div>
)}
) : null}
</div>
)}
) : null}
</div>
</div>
);
+2 -2
View File
@@ -140,13 +140,13 @@ export function InitialSetupView() {
</AlertDescription>
</Alert>
{registerMutation.isError && (
{registerMutation.isError ? (
<Alert variant="destructive" className="mb-6 animate-slide-in-down">
<AlertDescription>
{getErrorMessage(registerMutation.error) || 'Setup failed. Please try again.'}
</AlertDescription>
</Alert>
)}
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="relative space-y-4">
+6 -6
View File
@@ -103,23 +103,23 @@ export function JobsView() {
</div>
{/* Loading State */}
{isLoading && (
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[1, 2].map((i) => (
<Skeleton key={i} className="h-64 w-full" />
))}
</div>
)}
) : null}
{/* Error State */}
{error && (
{error ? (
<div className="bg-red-950/20 border border-red-500 rounded-lg p-4">
<p className="text-red-400">Failed to load jobs. Please try again.</p>
</div>
)}
) : null}
{/* Jobs Grid */}
{!isLoading && !error && (
{!isLoading && !error ? (
<>
{jobs.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
@@ -143,7 +143,7 @@ export function JobsView() {
</div>
)}
</>
)}
) : null}
{/* Logs Dialog */}
<Dialog open={showLogsDialog} onOpenChange={setShowLogsDialog}>
+6 -6
View File
@@ -78,11 +78,11 @@ export function MaintenancePage() {
</div>
{/* User info */}
{user && (
{user ? (
<div className="text-sm text-muted-foreground">
Logged in as: <span className="font-medium">{user.username}</span>
</div>
)}
) : null}
{/* Actions */}
<div className="space-y-3">
@@ -94,16 +94,16 @@ export function MaintenancePage() {
Refresh Page
</Button>
{!user && (
{!user ? (
<Button
className="w-full"
onClick={() => navigate('/login')}
>
Login
</Button>
)}
) : null}
{user && (
{user ? (
<Button
variant="ghost"
className="w-full"
@@ -111,7 +111,7 @@ export function MaintenancePage() {
>
Logout
</Button>
)}
) : null}
</div>
</div>
</div>
+4 -4
View File
@@ -54,12 +54,12 @@ export function PlaylistDetailView() {
<h1 className="text-3xl font-bold">{playlist.title}</h1>
<Badge variant="secondary">Flashpoint</Badge>
</div>
{playlist.description && (
{playlist.description ? (
<p className="text-muted-foreground">{playlist.description}</p>
)}
{playlist.author && (
) : null}
{playlist.author ? (
<p className="text-sm text-muted-foreground mt-1">By {playlist.author}</p>
)}
) : null}
</div>
{playlist.games && playlist.games.length > 0 ? (
+4 -4
View File
@@ -117,16 +117,16 @@ export function PlaylistsView() {
</Link>
<Link to={`/flashpoint-playlists/${playlist.id}`} className="flex-1 min-w-0">
<h3 className="font-semibold mb-1">{playlist.title}</h3>
{playlist.description && (
{playlist.description ? (
<p className="text-sm text-muted-foreground line-clamp-2">
{playlist.description}
</p>
)}
{playlist.gameIds && (
) : null}
{playlist.gameIds ? (
<p className="text-xs text-muted-foreground mt-2">
{playlist.gameIds.length} games
</p>
)}
) : null}
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
+4 -4
View File
@@ -46,21 +46,21 @@ export function RolesView() {
<RoleTable onEdit={handleEdit} onManagePermissions={handleManagePermissions} />
{showRoleForm && (
{showRoleForm ? (
<RoleForm
role={editingRole}
onClose={handleCloseForm}
onSuccess={handleSuccess}
/>
)}
) : null}
{managingPermissionsRole && (
{managingPermissionsRole ? (
<PermissionSelector
role={managingPermissionsRole}
onClose={() => setManagingPermissionsRole(null)}
onSuccess={handleSuccess}
/>
)}
) : null}
</div>
);
}
+8 -8
View File
@@ -81,21 +81,21 @@ export function SettingsView() {
<div className="mt-6">
<AnimatePresence mode="wait">
{activeTab === "general" && (
{activeTab === "general" ? (
<GeneralSettingsTab tabContentVariants={tabContentVariants} />
)}
) : null}
{activeTab === "app" && (
{activeTab === "app" ? (
<AppSettingsTab tabContentVariants={tabContentVariants} />
)}
) : null}
{activeTab === "update" && (
{activeTab === "update" ? (
<UpdateSettingsTab tabContentVariants={tabContentVariants} />
)}
) : null}
{activeTab === "features" && (
{activeTab === "features" ? (
<FeaturesSettingsTab tabContentVariants={tabContentVariants} />
)}
) : null}
</AnimatePresence>
</div>
</Tabs>
+10 -10
View File
@@ -153,7 +153,7 @@ export function SharedPlaylistView() {
</Alert>
{/* Back Button (if authenticated) */}
{isAuthenticated && (
{isAuthenticated ? (
<Link
to="/playlists"
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
@@ -161,13 +161,13 @@ export function SharedPlaylistView() {
<ArrowLeft size={20} />
Back to my playlists
</Link>
)}
) : null}
{/* Playlist Header */}
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex gap-4">
{/* Playlist Icon */}
{playlist.icon && (
{playlist.icon ? (
<div className="flex-shrink-0">
<div className="p-4 bg-primary/20 rounded-xl border-2 border-primary/30">
<PlaylistIcon
@@ -178,7 +178,7 @@ export function SharedPlaylistView() {
/>
</div>
</div>
)}
) : null}
{/* Playlist Info */}
<div className="flex-1">
@@ -186,18 +186,18 @@ export function SharedPlaylistView() {
<h1 className="text-3xl font-bold">{playlist.title}</h1>
<Badge variant="secondary">Shared</Badge>
</div>
{playlist.description && (
{playlist.description ? (
<p className="text-muted-foreground">{playlist.description}</p>
)}
) : null}
<div className="flex items-center gap-4 mt-2">
<p className="text-sm text-muted-foreground">
{playlist.gameCount} {playlist.gameCount === 1 ? 'game' : 'games'}
</p>
{playlist.ownerUsername && (
{playlist.ownerUsername ? (
<p className="text-sm text-muted-foreground">
Created by <span className="font-medium">{playlist.ownerUsername}</span>
</p>
)}
) : null}
</div>
</div>
</div>
@@ -205,14 +205,14 @@ export function SharedPlaylistView() {
{/* Actions */}
<div className="flex items-center gap-2">
<ViewOptions />
{isAuthenticated && (
{isAuthenticated ? (
<Button
onClick={handleClone}
disabled={isCloning || clonePlaylist.isPending}
>
{isCloning || clonePlaylist.isPending ? 'Cloning...' : 'Clone to My Playlists'}
</Button>
)}
) : null}
</div>
</div>
+12 -12
View File
@@ -76,7 +76,7 @@ export function UnauthorizedView() {
</CardHeader>
<CardContent className="space-y-4">
{isAuthenticated && user && (
{isAuthenticated && user ? (
<Alert>
<AlertDescription>
<div className="space-y-1">
@@ -88,37 +88,37 @@ export function UnauthorizedView() {
</div>
</AlertDescription>
</Alert>
)}
) : null}
{(requiredPermission || requiredFeature) && (
{(requiredPermission || requiredFeature) ? (
<Alert>
<AlertDescription>
<div className="space-y-1">
{requiredPermission && (
{requiredPermission ? (
<p className="text-sm">
<span className="font-semibold">Required permission:</span>{' '}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
{requiredPermission}
</code>
</p>
)}
{requiredFeature && (
) : null}
{requiredFeature ? (
<p className="text-sm">
<span className="font-semibold">Required feature:</span>{' '}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
{requiredFeature}
</code>
</p>
)}
{fromPath && (
) : null}
{fromPath ? (
<p className="text-sm text-muted-foreground">
Attempted to access: {fromPath}
</p>
)}
) : null}
</div>
</AlertDescription>
</Alert>
)}
) : null}
<div className="space-y-2">
<h3 className="font-semibold text-sm">What you can do:</h3>
@@ -145,11 +145,11 @@ export function UnauthorizedView() {
<Button onClick={handleGoHome} variant="default">
Go to Home
</Button>
{isAuthenticated && (
{isAuthenticated ? (
<Button onClick={() => navigate('/settings')} variant="secondary">
View Settings
</Button>
)}
) : null}
</>
)}
</CardFooter>
@@ -116,7 +116,7 @@ export function UserPlaylistDetailView() {
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex gap-4">
{/* Playlist Icon */}
{playlist.icon && (
{playlist.icon ? (
<div className="flex-shrink-0">
<div className="p-4 bg-primary/20 rounded-xl border-2 border-primary/30">
<PlaylistIcon
@@ -127,19 +127,19 @@ export function UserPlaylistDetailView() {
/>
</div>
</div>
)}
) : null}
{/* Playlist Info */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold">{playlist.title}</h1>
{playlist.isPublic && (
{playlist.isPublic ? (
<Badge variant="secondary">Shared</Badge>
)}
) : null}
</div>
{playlist.description && (
{playlist.description ? (
<p className="text-muted-foreground">{playlist.description}</p>
)}
) : null}
<p className="text-sm text-muted-foreground mt-2">
{playlist.gameCount} {playlist.gameCount === 1 ? 'game' : 'games'}
</p>
+2 -2
View File
@@ -148,13 +148,13 @@ export function UserPlaylistsView() {
/>
{/* Share Dialog */}
{sharingPlaylist && (
{sharingPlaylist ? (
<SharePlaylistDialog
isOpen={!!sharingPlaylist}
onClose={() => setSharingPlaylist(null)}
playlist={sharingPlaylist}
/>
)}
) : null}
{/* Delete Confirmation Dialog */}
<AlertDialog
+4 -4
View File
@@ -46,22 +46,22 @@ export function UsersView() {
<UserTable onEdit={handleEdit} onChangePassword={handleChangePassword} />
{showUserForm && (
{showUserForm ? (
<UserForm
user={editingUser}
onClose={handleCloseForm}
onSuccess={handleSuccess}
/>
)}
) : null}
{changingPasswordUser && (
{changingPasswordUser ? (
<ChangePasswordDialog
isOpen={!!changingPasswordUser}
user={changingPasswordUser}
onClose={() => setChangingPasswordUser(null)}
onSuccess={handleSuccess}
/>
)}
) : null}
</div>
);
}