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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -148,13 +148,13 @@ export function UserPlaylistsView() {
|
||||
/>
|
||||
|
||||
{/* Share Dialog */}
|
||||
{sharingPlaylist && (
|
||||
{sharingPlaylist ? (
|
||||
<SharePlaylistDialog
|
||||
isOpen={!!sharingPlaylist}
|
||||
onClose={() => setSharingPlaylist(null)}
|
||||
playlist={sharingPlaylist}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user