feat: initialize new React application structure with TypeScript, ESLint, and Prettier configurations, while removing legacy files and adding new components and pages for enhanced functionality

This commit is contained in:
Supra4E8C
2025-12-07 11:32:31 +08:00
parent 8e4132200d
commit 450964fb1a
144 changed files with 14223 additions and 21647 deletions

View File

@@ -0,0 +1,39 @@
import type { ButtonHTMLAttributes, PropsWithChildren } from 'react';
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'md' | 'sm';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
fullWidth?: boolean;
loading?: boolean;
}
export function Button({
children,
variant = 'primary',
size = 'md',
fullWidth = false,
loading = false,
className = '',
disabled,
...rest
}: PropsWithChildren<ButtonProps>) {
const classes = [
'btn',
`btn-${variant}`,
size === 'sm' ? 'btn-sm' : '',
fullWidth ? 'btn-full' : '',
className
]
.filter(Boolean)
.join(' ');
return (
<button className={classes} disabled={disabled || loading} {...rest}>
{loading && <span className="loading-spinner" aria-hidden="true" />}
<span>{children}</span>
</button>
);
}

View File

@@ -0,0 +1,20 @@
import type { PropsWithChildren, ReactNode } from 'react';
interface CardProps {
title?: ReactNode;
extra?: ReactNode;
}
export function Card({ title, extra, children }: PropsWithChildren<CardProps>) {
return (
<div className="card">
{(title || extra) && (
<div className="card-header">
<div className="title">{title}</div>
{extra}
</div>
)}
{children}
</div>
);
}

View File

@@ -0,0 +1,24 @@
import type { ReactNode } from 'react';
interface EmptyStateProps {
title: string;
description?: string;
action?: ReactNode;
}
export function EmptyState({ title, description, action }: EmptyStateProps) {
return (
<div className="empty-state">
<div className="empty-content">
<div className="empty-icon" aria-hidden="true">
</div>
<div>
<div className="empty-title">{title}</div>
{description && <div className="empty-desc">{description}</div>}
</div>
</div>
{action && <div className="empty-action">{action}</div>}
</div>
);
}

View File

@@ -0,0 +1,74 @@
import { Fragment } from 'react';
import { Button } from './Button';
import type { HeaderEntry } from '@/utils/headers';
interface HeaderInputListProps {
entries: HeaderEntry[];
onChange: (entries: HeaderEntry[]) => void;
addLabel: string;
disabled?: boolean;
keyPlaceholder?: string;
valuePlaceholder?: string;
}
export function HeaderInputList({
entries,
onChange,
addLabel,
disabled = false,
keyPlaceholder = 'X-Custom-Header',
valuePlaceholder = 'value'
}: HeaderInputListProps) {
const currentEntries = entries.length ? entries : [{ key: '', value: '' }];
const updateEntry = (index: number, field: 'key' | 'value', value: string) => {
const next = currentEntries.map((entry, idx) => (idx === index ? { ...entry, [field]: value } : entry));
onChange(next);
};
const addEntry = () => {
onChange([...currentEntries, { key: '', value: '' }]);
};
const removeEntry = (index: number) => {
const next = currentEntries.filter((_, idx) => idx !== index);
onChange(next.length ? next : [{ key: '', value: '' }]);
};
return (
<div className="header-input-list">
{currentEntries.map((entry, index) => (
<Fragment key={index}>
<div className="header-input-row">
<input
className="input"
placeholder={keyPlaceholder}
value={entry.key}
onChange={(e) => updateEntry(index, 'key', e.target.value)}
disabled={disabled}
/>
<span className="header-separator">:</span>
<input
className="input"
placeholder={valuePlaceholder}
value={entry.value}
onChange={(e) => updateEntry(index, 'value', e.target.value)}
disabled={disabled}
/>
<Button
variant="ghost"
size="sm"
onClick={() => removeEntry(index)}
disabled={disabled || currentEntries.length <= 1}
>
</Button>
</div>
</Fragment>
))}
<Button variant="secondary" size="sm" onClick={addEntry} disabled={disabled} className="align-start">
{addLabel}
</Button>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import type { InputHTMLAttributes, ReactNode } from 'react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
hint?: string;
error?: string;
rightElement?: ReactNode;
}
export function Input({ label, hint, error, rightElement, className = '', ...rest }: InputProps) {
return (
<div className="form-group">
{label && <label>{label}</label>}
<div style={{ position: 'relative' }}>
<input className={`input ${className}`.trim()} {...rest} />
{rightElement && (
<div style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)' }}>
{rightElement}
</div>
)}
</div>
{hint && <div className="hint">{hint}</div>}
{error && <div className="error-box">{error}</div>}
</div>
);
}

View File

@@ -0,0 +1,10 @@
export function LoadingSpinner({ size = 20 }: { size?: number }) {
return (
<div
className="loading-spinner"
style={{ width: size, height: size, borderWidth: size / 7 }}
role="status"
aria-live="polite"
/>
);
}

View File

@@ -0,0 +1,34 @@
import type { PropsWithChildren, ReactNode } from 'react';
interface ModalProps {
open: boolean;
title?: ReactNode;
onClose: () => void;
footer?: ReactNode;
width?: number | string;
}
export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren<ModalProps>) {
if (!open) return null;
const handleMaskClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
onClose();
}
};
return (
<div className="modal-overlay" onClick={handleMaskClick}>
<div className="modal" style={{ width }} role="dialog" aria-modal="true">
<div className="modal-header">
<div className="modal-title">{title}</div>
<button className="modal-close" onClick={onClose} aria-label="Close">
×
</button>
</div>
<div className="modal-body">{children}</div>
{footer && <div className="modal-footer">{footer}</div>}
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import type { ChangeEvent } from 'react';
interface ToggleSwitchProps {
checked: boolean;
onChange: (value: boolean) => void;
label?: string;
disabled?: boolean;
}
export function ToggleSwitch({ checked, onChange, label, disabled = false }: ToggleSwitchProps) {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.checked);
};
return (
<label className="switch">
<input type="checkbox" checked={checked} onChange={handleChange} disabled={disabled} />
<span className="track">
<span className="thumb" />
</span>
{label && <span className="label">{label}</span>}
</label>
);
}