mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-28 22:32:49 -05:00
Formatting
This commit is contained in:
@@ -39,14 +39,14 @@ const ActionButton: FC<ActionButtonProps> = ({
|
||||
const getButtonVariant = (): ButtonVariant => {
|
||||
// For uninstall actions, use danger variant
|
||||
if (isInstalled) return 'danger'
|
||||
// For install actions, use success variant
|
||||
// For install actions, use success variant
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// Select appropriate icon based on action type and state
|
||||
const getIconInfo = () => {
|
||||
const isCream = action.includes('cream')
|
||||
|
||||
|
||||
if (isInstalled) {
|
||||
// Uninstall actions
|
||||
return { name: layers, variant: 'bold' }
|
||||
@@ -66,17 +66,13 @@ const ActionButton: FC<ActionButtonProps> = ({
|
||||
disabled={disabled || isWorking}
|
||||
fullWidth
|
||||
className={`action-button ${className}`}
|
||||
leftIcon={isWorking ? undefined : (
|
||||
<Icon
|
||||
name={iconInfo.name}
|
||||
variant={iconInfo.variant}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
leftIcon={
|
||||
isWorking ? undefined : <Icon name={iconInfo.name} variant={iconInfo.variant} size="md" />
|
||||
}
|
||||
>
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionButton
|
||||
export default ActionButton
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Icon, check } from '@/components/icons'
|
||||
|
||||
interface AnimatedCheckboxProps {
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
label?: string;
|
||||
sublabel?: string;
|
||||
className?: string;
|
||||
checked: boolean
|
||||
onChange: () => void
|
||||
label?: string
|
||||
sublabel?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,24 +20,12 @@ const AnimatedCheckbox = ({
|
||||
}: AnimatedCheckboxProps) => {
|
||||
return (
|
||||
<label className={`animated-checkbox ${className}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className="checkbox-original"
|
||||
/>
|
||||
|
||||
<input type="checkbox" checked={checked} onChange={onChange} className="checkbox-original" />
|
||||
|
||||
<span className={`checkbox-custom ${checked ? 'checked' : ''}`}>
|
||||
{checked && (
|
||||
<Icon
|
||||
name={check}
|
||||
variant="bold"
|
||||
size="sm"
|
||||
className="checkbox-icon"
|
||||
/>
|
||||
)}
|
||||
{checked && <Icon name={check} variant="bold" size="sm" className="checkbox-icon" />}
|
||||
</span>
|
||||
|
||||
|
||||
{(label || sublabel) && (
|
||||
<div className="checkbox-content">
|
||||
{label && <span className="checkbox-label">{label}</span>}
|
||||
@@ -48,4 +36,4 @@ const AnimatedCheckbox = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedCheckbox
|
||||
export default AnimatedCheckbox
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { FC, ButtonHTMLAttributes } from 'react';
|
||||
import { FC, ButtonHTMLAttributes } from 'react'
|
||||
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning';
|
||||
export type ButtonSize = 'small' | 'medium' | 'large';
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning'
|
||||
export type ButtonSize = 'small' | 'medium' | 'large'
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
isLoading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
fullWidth?: boolean;
|
||||
variant?: ButtonVariant
|
||||
size?: ButtonSize
|
||||
isLoading?: boolean
|
||||
leftIcon?: React.ReactNode
|
||||
rightIcon?: React.ReactNode
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ const Button: FC<ButtonProps> = ({
|
||||
small: 'btn-sm',
|
||||
medium: 'btn-md',
|
||||
large: 'btn-lg',
|
||||
}[size];
|
||||
}[size]
|
||||
|
||||
// Variant class mapping
|
||||
const variantClass = {
|
||||
@@ -41,7 +41,7 @@ const Button: FC<ButtonProps> = ({
|
||||
danger: 'btn-danger',
|
||||
success: 'btn-success',
|
||||
warning: 'btn-warning',
|
||||
}[variant];
|
||||
}[variant]
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -56,12 +56,12 @@ const Button: FC<ButtonProps> = ({
|
||||
<span className="spinner"></span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
||||
{leftIcon && !isLoading && <span className="btn-icon btn-icon-left">{leftIcon}</span>}
|
||||
<span className="btn-text">{children}</span>
|
||||
{rightIcon && !isLoading && <span className="btn-icon btn-icon-right">{rightIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Button;
|
||||
export default Button
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Export all button components
|
||||
export { default as Button } from './Button';
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as AnimatedCheckbox } from './AnimatedCheckbox';
|
||||
export { default as Button } from './Button'
|
||||
export { default as ActionButton } from './ActionButton'
|
||||
export { default as AnimatedCheckbox } from './AnimatedCheckbox'
|
||||
|
||||
// Export types
|
||||
export type { ButtonVariant, ButtonSize } from './Button';
|
||||
export type { ActionType } from './ActionButton';
|
||||
export type { ButtonVariant, ButtonSize } from './Button'
|
||||
export type { ActionType } from './ActionButton'
|
||||
|
||||
@@ -4,11 +4,11 @@ export type LoadingType = 'spinner' | 'dots' | 'progress'
|
||||
export type LoadingSize = 'small' | 'medium' | 'large'
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
size?: LoadingSize;
|
||||
type?: LoadingType;
|
||||
message?: string;
|
||||
progress?: number;
|
||||
className?: string;
|
||||
size?: LoadingSize
|
||||
type?: LoadingType
|
||||
message?: string
|
||||
progress?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ const LoadingIndicator = ({
|
||||
switch (type) {
|
||||
case 'spinner':
|
||||
return <div className="loading-spinner"></div>
|
||||
|
||||
|
||||
case 'dots':
|
||||
return (
|
||||
<div className="loading-dots">
|
||||
@@ -43,7 +43,7 @@ const LoadingIndicator = ({
|
||||
<div className="dot dot-3"></div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
case 'progress':
|
||||
return (
|
||||
<div className="loading-progress">
|
||||
@@ -53,12 +53,10 @@ const LoadingIndicator = ({
|
||||
style={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
{progress > 0 && (
|
||||
<div className="progress-percentage">{Math.round(progress)}%</div>
|
||||
)}
|
||||
{progress > 0 && <div className="progress-percentage">{Math.round(progress)}%</div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
return <div className="loading-spinner"></div>
|
||||
}
|
||||
@@ -72,4 +70,4 @@ const LoadingIndicator = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadingIndicator
|
||||
export default LoadingIndicator
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||
export { default as LoadingIndicator } from './LoadingIndicator'
|
||||
|
||||
export type { LoadingSize, LoadingType } from './LoadingIndicator';
|
||||
export type { LoadingSize, LoadingType } from './LoadingIndicator'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
|
||||
export interface DialogProps {
|
||||
visible: boolean;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
preventBackdropClose?: boolean;
|
||||
children: ReactNode;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
showAnimationOnUnmount?: boolean;
|
||||
visible: boolean
|
||||
onClose?: () => void
|
||||
className?: string
|
||||
preventBackdropClose?: boolean
|
||||
children: ReactNode
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
showAnimationOnUnmount?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,17 +66,12 @@ const Dialog = ({
|
||||
}[size]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`dialog-overlay ${showContent ? 'visible' : ''}`}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div
|
||||
className={`dialog ${sizeClass} ${className} ${showContent ? 'dialog-visible' : ''}`}
|
||||
>
|
||||
<div className={`dialog-overlay ${showContent ? 'visible' : ''}`} onClick={handleBackdropClick}>
|
||||
<div className={`dialog ${sizeClass} ${className} ${showContent ? 'dialog-visible' : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dialog
|
||||
export default Dialog
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export interface DialogActionsProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
children: ReactNode
|
||||
className?: string
|
||||
align?: 'start' | 'center' | 'end'
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions container for dialog footers
|
||||
* Provides consistent spacing and alignment for action buttons
|
||||
*/
|
||||
const DialogActions = ({
|
||||
children,
|
||||
className = '',
|
||||
align = 'end'
|
||||
}: DialogActionsProps) => {
|
||||
const DialogActions = ({ children, className = '', align = 'end' }: DialogActionsProps) => {
|
||||
const alignClass = {
|
||||
start: 'justify-start',
|
||||
center: 'justify-center',
|
||||
end: 'justify-end'
|
||||
}[align];
|
||||
end: 'justify-end',
|
||||
}[align]
|
||||
|
||||
return (
|
||||
<div className={`dialog-actions ${alignClass} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className={`dialog-actions ${alignClass} ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
export default DialogActions
|
||||
export default DialogActions
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export interface DialogBodyProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10,11 +10,7 @@ export interface DialogBodyProps {
|
||||
* Contains the main content with scrolling capability
|
||||
*/
|
||||
const DialogBody = ({ children, className = '' }: DialogBodyProps) => {
|
||||
return (
|
||||
<div className={`dialog-body ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className={`dialog-body ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
export default DialogBody
|
||||
export default DialogBody
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export interface DialogFooterProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10,11 +10,7 @@ export interface DialogFooterProps {
|
||||
* Contains action buttons and optional status information
|
||||
*/
|
||||
const DialogFooter = ({ children, className = '' }: DialogFooterProps) => {
|
||||
return (
|
||||
<div className={`dialog-footer ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className={`dialog-footer ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
export default DialogFooter
|
||||
export default DialogFooter
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export interface DialogHeaderProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,11 +15,7 @@ const DialogHeader = ({ children, className = '', onClose }: DialogHeaderProps)
|
||||
<div className={`dialog-header ${className}`}>
|
||||
{children}
|
||||
{onClose && (
|
||||
<button
|
||||
className="dialog-close-button"
|
||||
onClick={onClose}
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<button className="dialog-close-button" onClick={onClose} aria-label="Close dialog">
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
@@ -27,4 +23,4 @@ const DialogHeader = ({ children, className = '', onClose }: DialogHeaderProps)
|
||||
)
|
||||
}
|
||||
|
||||
export default DialogHeader
|
||||
export default DialogHeader
|
||||
|
||||
@@ -8,15 +8,15 @@ import { Button, AnimatedCheckbox } from '@/components/buttons'
|
||||
import { DlcInfo } from '@/types'
|
||||
|
||||
export interface DlcSelectionDialogProps {
|
||||
visible: boolean;
|
||||
gameTitle: string;
|
||||
dlcs: DlcInfo[];
|
||||
onClose: () => void;
|
||||
onConfirm: (selectedDlcs: DlcInfo[]) => void;
|
||||
isLoading: boolean;
|
||||
isEditMode?: boolean;
|
||||
loadingProgress?: number;
|
||||
estimatedTimeLeft?: string;
|
||||
visible: boolean
|
||||
gameTitle: string
|
||||
dlcs: DlcInfo[]
|
||||
onClose: () => void
|
||||
onConfirm: (selectedDlcs: DlcInfo[]) => void
|
||||
isLoading: boolean
|
||||
isEditMode?: boolean
|
||||
loadingProgress?: number
|
||||
estimatedTimeLeft?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,18 +56,18 @@ const DlcSelectionDialog = ({
|
||||
if (!initialized) {
|
||||
// Create a new array to ensure we don't share references
|
||||
setSelectedDlcs([...dlcs])
|
||||
|
||||
|
||||
// Determine initial selectAll state based on if all DLCs are enabled
|
||||
const allSelected = dlcs.every((dlc) => dlc.enabled)
|
||||
setSelectAll(allSelected)
|
||||
|
||||
|
||||
// Mark as initialized to avoid resetting selections on subsequent updates
|
||||
setInitialized(true)
|
||||
} else {
|
||||
// Find new DLCs that aren't in our current selection
|
||||
// Find new DLCs that aren't in our current selection
|
||||
const currentAppIds = new Set(selectedDlcs.map((dlc) => dlc.appid))
|
||||
const newDlcs = dlcs.filter((dlc) => !currentAppIds.has(dlc.appid))
|
||||
|
||||
|
||||
// If we found new DLCs, add them to our selection
|
||||
if (newDlcs.length > 0) {
|
||||
setSelectedDlcs((prev) => [...prev, ...newDlcs])
|
||||
@@ -118,9 +118,9 @@ const DlcSelectionDialog = ({
|
||||
// Submit selected DLCs to parent component
|
||||
const handleConfirm = useCallback(() => {
|
||||
// Create a deep copy to prevent reference issues
|
||||
const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs));
|
||||
onConfirm(dlcsCopy);
|
||||
}, [onConfirm, selectedDlcs]);
|
||||
const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs))
|
||||
onConfirm(dlcsCopy)
|
||||
}, [onConfirm, selectedDlcs])
|
||||
|
||||
// Count selected DLCs
|
||||
const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length
|
||||
@@ -128,7 +128,7 @@ const DlcSelectionDialog = ({
|
||||
// Format dialog title and messages based on mode
|
||||
const dialogTitle = isEditMode ? 'Edit DLCs' : 'Select DLCs to Enable'
|
||||
const actionButtonText = isEditMode ? 'Save Changes' : 'Install with Selected DLCs'
|
||||
|
||||
|
||||
// Format loading message to show total number of DLCs found
|
||||
const getLoadingInfoText = () => {
|
||||
if (isLoading && loadingProgress < 100) {
|
||||
@@ -140,12 +140,7 @@ const DlcSelectionDialog = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
size="large"
|
||||
preventBackdropClose={isLoading}
|
||||
>
|
||||
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
|
||||
<DialogHeader onClose={onClose}>
|
||||
<h3>{dialogTitle}</h3>
|
||||
<div className="dlc-game-info">
|
||||
@@ -224,11 +219,7 @@ const DlcSelectionDialog = ({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button variant="primary" onClick={handleConfirm} disabled={isLoading}>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -237,4 +228,4 @@ const DlcSelectionDialog = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default DlcSelectionDialog
|
||||
export default DlcSelectionDialog
|
||||
|
||||
@@ -7,20 +7,20 @@ import DialogActions from './DialogActions'
|
||||
import { Button } from '@/components/buttons'
|
||||
|
||||
export interface InstallationInstructions {
|
||||
type: string;
|
||||
command: string;
|
||||
game_title: string;
|
||||
dlc_count?: number;
|
||||
type: string
|
||||
command: string
|
||||
game_title: string
|
||||
dlc_count?: number
|
||||
}
|
||||
|
||||
export interface ProgressDialogProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
progress: number;
|
||||
showInstructions?: boolean;
|
||||
instructions?: InstallationInstructions;
|
||||
onClose?: () => void;
|
||||
visible: boolean
|
||||
title: string
|
||||
message: string
|
||||
progress: number
|
||||
showInstructions?: boolean
|
||||
instructions?: InstallationInstructions
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +126,7 @@ const ProgressDialog = ({
|
||||
<DialogHeader>
|
||||
<h3>{title}</h3>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<DialogBody>
|
||||
<p>{message}</p>
|
||||
|
||||
@@ -150,24 +150,17 @@ const ProgressDialog = ({
|
||||
</div>
|
||||
)}
|
||||
</DialogBody>
|
||||
|
||||
|
||||
<DialogFooter>
|
||||
<DialogActions>
|
||||
{showInstructions && showCopyButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCopyCommand}
|
||||
>
|
||||
<Button variant="primary" onClick={handleCopyCommand}>
|
||||
{copySuccess ? 'Copied!' : 'Copy to Clipboard'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isCloseButtonEnabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
disabled={!isCloseButtonEnabled}
|
||||
>
|
||||
<Button variant="secondary" onClick={onClose} disabled={!isCloseButtonEnabled}>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
@@ -177,4 +170,4 @@ const ProgressDialog = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgressDialog
|
||||
export default ProgressDialog
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Export all dialog components
|
||||
export { default as Dialog } from './Dialog';
|
||||
export { default as DialogHeader } from './DialogHeader';
|
||||
export { default as DialogBody } from './DialogBody';
|
||||
export { default as DialogFooter } from './DialogFooter';
|
||||
export { default as DialogActions } from './DialogActions';
|
||||
export { default as ProgressDialog } from './ProgressDialog';
|
||||
export { default as DlcSelectionDialog } from './DlcSelectionDialog';
|
||||
// Export all dialog components
|
||||
export { default as Dialog } from './Dialog'
|
||||
export { default as DialogHeader } from './DialogHeader'
|
||||
export { default as DialogBody } from './DialogBody'
|
||||
export { default as DialogFooter } from './DialogFooter'
|
||||
export { default as DialogActions } from './DialogActions'
|
||||
export { default as ProgressDialog } from './ProgressDialog'
|
||||
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
||||
|
||||
// Export types
|
||||
export type { DialogProps } from './Dialog';
|
||||
export type { DialogHeaderProps } from './DialogHeader';
|
||||
export type { DialogBodyProps } from './DialogBody';
|
||||
export type { DialogFooterProps } from './DialogFooter';
|
||||
export type { DialogActionsProps } from './DialogActions';
|
||||
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog';
|
||||
export type { DlcSelectionDialogProps } from './DlcSelectionDialog';
|
||||
export type { DialogProps } from './Dialog'
|
||||
export type { DialogHeaderProps } from './DialogHeader'
|
||||
export type { DialogBodyProps } from './DialogBody'
|
||||
export type { DialogFooterProps } from './DialogFooter'
|
||||
export type { DialogActionsProps } from './DialogActions'
|
||||
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
||||
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
||||
|
||||
@@ -161,4 +161,4 @@ const GameItem = ({ game, onAction, onEdit }: GameItemProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default GameItem
|
||||
export default GameItem
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import {GameItem, ImagePreloader} from '@/components/games'
|
||||
import { GameItem, ImagePreloader } from '@/components/games'
|
||||
import { ActionType } from '@/components/buttons'
|
||||
import { Game } from '@/types'
|
||||
import LoadingIndicator from '../common/LoadingIndicator'
|
||||
@@ -18,7 +18,7 @@ interface GameListProps {
|
||||
const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
|
||||
const [imagesPreloaded, setImagesPreloaded] = useState(false)
|
||||
|
||||
// Sort games alphabetically by title
|
||||
// Sort games alphabetically by title
|
||||
const sortedGames = useMemo(() => {
|
||||
return [...games].sort((a, b) => a.title.localeCompare(b.title))
|
||||
}, [games])
|
||||
@@ -35,11 +35,7 @@ const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="game-list">
|
||||
<LoadingIndicator
|
||||
type="spinner"
|
||||
size="large"
|
||||
message="Scanning for games..."
|
||||
/>
|
||||
<LoadingIndicator type="spinner" size="large" message="Scanning for games..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -68,4 +64,4 @@ const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default GameList
|
||||
export default GameList
|
||||
|
||||
@@ -16,24 +16,24 @@ const ImagePreloader = ({ gameIds, onComplete }: ImagePreloaderProps) => {
|
||||
try {
|
||||
// Only preload the first batch for performance (10 images max)
|
||||
const batchToPreload = gameIds.slice(0, 10)
|
||||
|
||||
|
||||
// Track loading progress
|
||||
let loadedCount = 0
|
||||
const totalImages = batchToPreload.length
|
||||
|
||||
|
||||
// Load images in parallel
|
||||
await Promise.allSettled(
|
||||
batchToPreload.map(async (id) => {
|
||||
await findBestGameImage(id)
|
||||
loadedCount++
|
||||
|
||||
|
||||
// If all images are loaded, call onComplete
|
||||
if (loadedCount === totalImages && onComplete) {
|
||||
onComplete()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
// Fallback if Promise.allSettled doesn't trigger onComplete
|
||||
if (onComplete) {
|
||||
onComplete()
|
||||
@@ -58,4 +58,4 @@ const ImagePreloader = ({ gameIds, onComplete }: ImagePreloaderProps) => {
|
||||
return null
|
||||
}
|
||||
|
||||
export default ImagePreloader
|
||||
export default ImagePreloader
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Export all game components
|
||||
export { default as GameList } from './GameList';
|
||||
export { default as GameItem } from './GameItem';
|
||||
export { default as ImagePreloader } from './ImagePreloader';
|
||||
export { default as GameList } from './GameList'
|
||||
export { default as GameItem } from './GameItem'
|
||||
export { default as ImagePreloader } from './ImagePreloader'
|
||||
|
||||
@@ -45,7 +45,7 @@ const getSizeValue = (size: IconSize): string => {
|
||||
sm: '16px',
|
||||
md: '24px',
|
||||
lg: '32px',
|
||||
xl: '48px'
|
||||
xl: '48px',
|
||||
}
|
||||
|
||||
return sizeMap[size] || sizeMap.md
|
||||
@@ -54,11 +54,15 @@ const getSizeValue = (size: IconSize): string => {
|
||||
/**
|
||||
* Gets the icon component based on name and variant
|
||||
*/
|
||||
const getIconComponent = (name: string, variant: IconVariant | string): React.ComponentType<React.SVGProps<SVGSVGElement>> | null => {
|
||||
const getIconComponent = (
|
||||
name: string,
|
||||
variant: IconVariant | string
|
||||
): React.ComponentType<React.SVGProps<SVGSVGElement>> | null => {
|
||||
// Normalize variant to ensure it's a valid IconVariant
|
||||
const normalizedVariant = (variant === 'bold' || variant === 'outline' || variant === 'brand')
|
||||
? variant as IconVariant
|
||||
: undefined;
|
||||
const normalizedVariant =
|
||||
variant === 'bold' || variant === 'outline' || variant === 'brand'
|
||||
? (variant as IconVariant)
|
||||
: undefined
|
||||
|
||||
// Try to get the icon from the specified variant
|
||||
switch (normalizedVariant) {
|
||||
@@ -97,7 +101,7 @@ const Icon: React.FC<IconProps> = ({
|
||||
}) => {
|
||||
// Determine default variant based on icon type if no variant provided
|
||||
let defaultVariant: IconVariant | string = variant
|
||||
|
||||
|
||||
if (defaultVariant === undefined) {
|
||||
if (BRAND_ICON_NAMES.has(name)) {
|
||||
defaultVariant = 'brand'
|
||||
@@ -105,17 +109,17 @@ const Icon: React.FC<IconProps> = ({
|
||||
defaultVariant = 'bold' // Default to bold for non-brand icons
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get the icon component based on name and variant
|
||||
let finalIconComponent = getIconComponent(name, defaultVariant)
|
||||
let finalVariant = defaultVariant
|
||||
|
||||
|
||||
// Try fallbacks if the icon doesn't exist in the requested variant
|
||||
if (!finalIconComponent && defaultVariant !== 'outline') {
|
||||
finalIconComponent = getIconComponent(name, 'outline')
|
||||
finalVariant = 'outline'
|
||||
}
|
||||
|
||||
|
||||
if (!finalIconComponent && defaultVariant !== 'bold') {
|
||||
finalIconComponent = getIconComponent(name, 'bold')
|
||||
finalVariant = 'bold'
|
||||
@@ -125,7 +129,7 @@ const Icon: React.FC<IconProps> = ({
|
||||
finalIconComponent = getIconComponent(name, 'brand')
|
||||
finalVariant = 'brand'
|
||||
}
|
||||
|
||||
|
||||
// If still no icon found, return null
|
||||
if (!finalIconComponent) {
|
||||
console.warn(`Icon not found: ${name} (${defaultVariant})`)
|
||||
@@ -134,7 +138,7 @@ const Icon: React.FC<IconProps> = ({
|
||||
|
||||
const sizeValue = getSizeValue(size)
|
||||
const combinedClassName = `icon icon-${size} icon-${finalVariant} ${className}`.trim()
|
||||
|
||||
|
||||
const IconComponentToRender = finalIconComponent
|
||||
|
||||
return (
|
||||
@@ -142,7 +146,7 @@ const Icon: React.FC<IconProps> = ({
|
||||
className={combinedClassName}
|
||||
width={sizeValue}
|
||||
height={sizeValue}
|
||||
fill={fillColor || 'currentColor'}
|
||||
fill={fillColor || 'currentColor'}
|
||||
stroke={strokeColor || 'currentColor'}
|
||||
role="img"
|
||||
aria-hidden={!title}
|
||||
@@ -152,4 +156,4 @@ const Icon: React.FC<IconProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default Icon
|
||||
export default Icon
|
||||
|
||||
@@ -21,4 +21,4 @@
|
||||
// IconComponent.displayName = `${name}Icon`
|
||||
// return IconComponent
|
||||
//}
|
||||
//
|
||||
//
|
||||
|
||||
@@ -4,4 +4,4 @@ export { ReactComponent as Steam } from './steam.svg'
|
||||
export { ReactComponent as Windows } from './windows.svg'
|
||||
export { ReactComponent as Github } from './github.svg'
|
||||
export { ReactComponent as Discord } from './discord.svg'
|
||||
export { ReactComponent as Proton } from './proton.svg'
|
||||
export { ReactComponent as Proton } from './proton.svg'
|
||||
|
||||
@@ -57,7 +57,7 @@ export const IconNames = {
|
||||
Warning: warning,
|
||||
Wine: wine,
|
||||
Diamond: diamond,
|
||||
|
||||
|
||||
// Brand icons
|
||||
Discord: discord,
|
||||
GitHub: github,
|
||||
@@ -97,4 +97,4 @@ export const IconNames = {
|
||||
//export const CheckBoldIcon = createIconComponent(check, 'bold')
|
||||
//export const InfoBoldIcon = createIconComponent(info, 'bold')
|
||||
//export const WarningBoldIcon = createIconComponent(warning, 'bold')
|
||||
//export const ErrorBoldIcon = createIconComponent(error, 'bold')
|
||||
//export const ErrorBoldIcon = createIconComponent(error, 'bold')
|
||||
|
||||
@@ -15,4 +15,4 @@ export { ReactComponent as Search } from './search.svg'
|
||||
export { ReactComponent as Trash } from './trash.svg'
|
||||
export { ReactComponent as Warning } from './warning.svg'
|
||||
export { ReactComponent as Wine } from './wine.svg'
|
||||
export { ReactComponent as Diamond } from './diamond.svg'
|
||||
export { ReactComponent as Diamond } from './diamond.svg'
|
||||
|
||||
@@ -15,4 +15,4 @@ export { ReactComponent as Search } from './search.svg'
|
||||
export { ReactComponent as Trash } from './trash.svg'
|
||||
export { ReactComponent as Warning } from './warning.svg'
|
||||
export { ReactComponent as Wine } from './wine.svg'
|
||||
export { ReactComponent as Diamond } from './diamond.svg'
|
||||
export { ReactComponent as Diamond } from './diamond.svg'
|
||||
|
||||
@@ -109,13 +109,7 @@ const AnimatedBackground = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="animated-background"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
return <canvas ref={canvasRef} className="animated-background" aria-hidden="true" />
|
||||
}
|
||||
|
||||
export default AnimatedBackground
|
||||
export default AnimatedBackground
|
||||
|
||||
@@ -2,14 +2,14 @@ import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
import { Button } from '@/components/buttons'
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
// Log the error
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo)
|
||||
|
||||
|
||||
// Call the onError callback if provided
|
||||
if (this.props.onError) {
|
||||
this.props.onError(error, errorInfo)
|
||||
@@ -52,22 +52,18 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
|
||||
// Default error UI
|
||||
return (
|
||||
<div className="error-container">
|
||||
<h2>Something went wrong</h2>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Error details</summary>
|
||||
<p>{this.state.error?.toString()}</p>
|
||||
</details>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={this.handleReset}
|
||||
className="error-retry-button"
|
||||
>
|
||||
|
||||
<Button variant="primary" onClick={this.handleReset} className="error-retry-button">
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
@@ -78,4 +74,4 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
export default ErrorBoundary
|
||||
|
||||
@@ -12,12 +12,7 @@ interface HeaderProps {
|
||||
* Application header component
|
||||
* Contains the app title, search input, and refresh button
|
||||
*/
|
||||
const Header = ({
|
||||
onRefresh,
|
||||
refreshDisabled = false,
|
||||
onSearch,
|
||||
searchQuery,
|
||||
}: HeaderProps) => {
|
||||
const Header = ({ onRefresh, refreshDisabled = false, onSearch, searchQuery }: HeaderProps) => {
|
||||
return (
|
||||
<header className="app-header">
|
||||
<div className="app-title">
|
||||
@@ -25,9 +20,9 @@ const Header = ({
|
||||
<h1>CreamLinux</h1>
|
||||
</div>
|
||||
<div className="header-controls">
|
||||
<Button
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onRefresh}
|
||||
onClick={onRefresh}
|
||||
disabled={refreshDisabled}
|
||||
className="refresh-button"
|
||||
leftIcon={<Icon name={refresh} variant="bold" size="md" />}
|
||||
@@ -49,4 +44,4 @@ const Header = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
export default Header
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface InitialLoadingScreenProps {
|
||||
message: string;
|
||||
progress: number;
|
||||
onComplete?: () => void;
|
||||
message: string
|
||||
progress: number
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11,36 +11,36 @@ interface InitialLoadingScreenProps {
|
||||
*/
|
||||
const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps) => {
|
||||
const [detailedStatus, setDetailedStatus] = useState<string[]>([
|
||||
"Initializing application...",
|
||||
"Setting up Steam integration...",
|
||||
"Preparing DLC management..."
|
||||
]);
|
||||
|
||||
'Initializing application...',
|
||||
'Setting up Steam integration...',
|
||||
'Preparing DLC management...',
|
||||
])
|
||||
|
||||
// Use a sequence of messages based on progress
|
||||
useEffect(() => {
|
||||
const messages = [
|
||||
{ threshold: 10, message: "Checking system requirements..." },
|
||||
{ threshold: 30, message: "Scanning Steam libraries..." },
|
||||
{ threshold: 50, message: "Discovering games..." },
|
||||
{ threshold: 70, message: "Analyzing game configurations..." },
|
||||
{ threshold: 90, message: "Preparing user interface..." },
|
||||
{ threshold: 100, message: "Ready to launch!" }
|
||||
];
|
||||
|
||||
{ threshold: 10, message: 'Checking system requirements...' },
|
||||
{ threshold: 30, message: 'Scanning Steam libraries...' },
|
||||
{ threshold: 50, message: 'Discovering games...' },
|
||||
{ threshold: 70, message: 'Analyzing game configurations...' },
|
||||
{ threshold: 90, message: 'Preparing user interface...' },
|
||||
{ threshold: 100, message: 'Ready to launch!' },
|
||||
]
|
||||
|
||||
// Find current status message based on progress
|
||||
const currentMessage = messages.find(m => progress <= m.threshold)?.message || "Loading...";
|
||||
|
||||
const currentMessage = messages.find((m) => progress <= m.threshold)?.message || 'Loading...'
|
||||
|
||||
// Add new messages to the log as progress increases
|
||||
if (currentMessage && !detailedStatus.includes(currentMessage)) {
|
||||
setDetailedStatus(prev => [...prev, currentMessage]);
|
||||
setDetailedStatus((prev) => [...prev, currentMessage])
|
||||
}
|
||||
}, [progress, detailedStatus]);
|
||||
}, [progress, detailedStatus])
|
||||
|
||||
return (
|
||||
<div className="initial-loading-screen">
|
||||
<div className="loading-content">
|
||||
<h1>CreamLinux</h1>
|
||||
|
||||
|
||||
<div className="loading-animation">
|
||||
{/* Enhanced animation with SVG or more elaborate CSS animation */}
|
||||
<div className="loading-circles">
|
||||
@@ -49,9 +49,9 @@ const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps)
|
||||
<div className="circle circle-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p className="loading-message">{message}</p>
|
||||
|
||||
|
||||
{/* Add a detailed status log that shows progress steps */}
|
||||
<div className="loading-status-log">
|
||||
{detailedStatus.slice(-4).map((status, index) => (
|
||||
@@ -61,15 +61,15 @@ const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps)
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="progress-percentage">{Math.round(progress)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default InitialLoadingScreen
|
||||
export default InitialLoadingScreen
|
||||
|
||||
@@ -22,29 +22,24 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
|
||||
const filters: FilterItem[] = [
|
||||
{ id: 'all', label: 'All Games', icon: layers, variant: 'bold' },
|
||||
{ id: 'native', label: 'Native', icon: linux, variant: 'brand' },
|
||||
{ id: 'proton', label: 'Proton Required', icon: proton, variant: 'brand' }
|
||||
{ id: 'proton', label: 'Proton Required', icon: proton, variant: 'brand' },
|
||||
]
|
||||
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h2>Library</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<ul className="filter-list">
|
||||
{filters.map(filter => (
|
||||
<li
|
||||
{filters.map((filter) => (
|
||||
<li
|
||||
key={filter.id}
|
||||
className={currentFilter === filter.id ? 'active' : ''}
|
||||
onClick={() => setFilter(filter.id)}
|
||||
>
|
||||
<div className="filter-item">
|
||||
<Icon
|
||||
name={filter.icon}
|
||||
variant={filter.variant}
|
||||
size="md"
|
||||
className="filter-icon"
|
||||
/>
|
||||
<Icon name={filter.icon} variant={filter.variant} size="md" className="filter-icon" />
|
||||
<span>{filter.label}</span>
|
||||
</div>
|
||||
</li>
|
||||
@@ -54,4 +49,4 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
export default Sidebar
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Export all layout components
|
||||
export { default as Header } from './Header';
|
||||
export { default as Sidebar } from './Sidebar';
|
||||
export { default as AnimatedBackground } from './AnimatedBackground';
|
||||
export { default as InitialLoadingScreen } from './InitialLoadingScreen';
|
||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||
export { default as Header } from './Header'
|
||||
export { default as Sidebar } from './Sidebar'
|
||||
export { default as AnimatedBackground } from './AnimatedBackground'
|
||||
export { default as InitialLoadingScreen } from './InitialLoadingScreen'
|
||||
export { default as ErrorBoundary } from './ErrorBoundary'
|
||||
|
||||
@@ -2,38 +2,38 @@ import { ReactNode, useState, useEffect, useCallback } from 'react'
|
||||
import { Icon, check, info, warning, error } from '@/components/icons'
|
||||
|
||||
export interface ToastProps {
|
||||
id: string;
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
title?: string;
|
||||
message: string;
|
||||
duration?: number;
|
||||
onDismiss: (id: string) => void;
|
||||
id: string
|
||||
type: 'success' | 'error' | 'warning' | 'info'
|
||||
title?: string
|
||||
message: string
|
||||
duration?: number
|
||||
onDismiss: (id: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual Toast component
|
||||
* Displays a notification message with automatic dismissal
|
||||
*/
|
||||
const Toast = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
duration = 5000, // default 5 seconds
|
||||
onDismiss
|
||||
const Toast = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
duration = 5000, // default 5 seconds
|
||||
onDismiss,
|
||||
}: ToastProps) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false)
|
||||
|
||||
// Use useCallback to memoize the handleDismiss function
|
||||
const handleDismiss = useCallback(() => {
|
||||
setIsClosing(true);
|
||||
setIsClosing(true)
|
||||
// Give time for exit animation
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
setTimeout(() => onDismiss(id), 50);
|
||||
}, 300);
|
||||
}, [id, onDismiss]);
|
||||
setVisible(false)
|
||||
setTimeout(() => onDismiss(id), 50)
|
||||
}, 300)
|
||||
}, [id, onDismiss])
|
||||
|
||||
// Handle animation on mount/unmount
|
||||
useEffect(() => {
|
||||
@@ -60,20 +60,22 @@ const Toast = ({
|
||||
const getIcon = (): ReactNode => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <Icon name={check} size="md" variant='bold' />
|
||||
return <Icon name={check} size="md" variant="bold" />
|
||||
case 'error':
|
||||
return <Icon name={error} size="md" variant='bold' />
|
||||
return <Icon name={error} size="md" variant="bold" />
|
||||
case 'warning':
|
||||
return <Icon name={warning} size="md" variant='bold' />
|
||||
return <Icon name={warning} size="md" variant="bold" />
|
||||
case 'info':
|
||||
return <Icon name={info} size="md" variant='bold' />
|
||||
return <Icon name={info} size="md" variant="bold" />
|
||||
default:
|
||||
return <Icon name={info} size="md" variant='bold' />
|
||||
return <Icon name={info} size="md" variant="bold" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`toast toast-${type} ${visible ? 'visible' : ''} ${isClosing ? 'closing' : ''}`}>
|
||||
<div
|
||||
className={`toast toast-${type} ${visible ? 'visible' : ''} ${isClosing ? 'closing' : ''}`}
|
||||
>
|
||||
<div className="toast-icon">{getIcon()}</div>
|
||||
<div className="toast-content">
|
||||
{title && <h4 className="toast-title">{title}</h4>}
|
||||
@@ -86,4 +88,4 @@ const Toast = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default Toast
|
||||
export default Toast
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import Toast, { ToastProps } from './Toast'
|
||||
|
||||
export type ToastPosition =
|
||||
| 'top-right'
|
||||
| 'top-left'
|
||||
| 'bottom-right'
|
||||
| 'bottom-left'
|
||||
| 'top-center'
|
||||
export type ToastPosition =
|
||||
| 'top-right'
|
||||
| 'top-left'
|
||||
| 'bottom-right'
|
||||
| 'bottom-left'
|
||||
| 'top-center'
|
||||
| 'bottom-center'
|
||||
|
||||
interface ToastContainerProps {
|
||||
toasts: Omit<ToastProps, 'onDismiss'>[];
|
||||
onDismiss: (id: string) => void;
|
||||
position?: ToastPosition;
|
||||
toasts: Omit<ToastProps, 'onDismiss'>[]
|
||||
onDismiss: (id: string) => void
|
||||
position?: ToastPosition
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for toast notifications
|
||||
* Manages positioning and rendering of all toast notifications
|
||||
*/
|
||||
const ToastContainer = ({
|
||||
toasts,
|
||||
onDismiss,
|
||||
position = 'bottom-right',
|
||||
}: ToastContainerProps) => {
|
||||
const ToastContainer = ({ toasts, onDismiss, position = 'bottom-right' }: ToastContainerProps) => {
|
||||
if (toasts.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={`toast-container ${position}`}>
|
||||
{toasts.map((toast) => (
|
||||
@@ -44,4 +40,4 @@ const ToastContainer = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default ToastContainer
|
||||
export default ToastContainer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { default as Toast } from './Toast';
|
||||
export { default as ToastContainer } from './ToastContainer';
|
||||
export { default as Toast } from './Toast'
|
||||
export { default as ToastContainer } from './ToastContainer'
|
||||
|
||||
export type { ToastProps } from './Toast';
|
||||
export type { ToastPosition } from './ToastContainer';
|
||||
export type { ToastProps } from './Toast'
|
||||
export type { ToastPosition } from './ToastContainer'
|
||||
|
||||
Reference in New Issue
Block a user