mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-28 16:35:11 +01:00
- Updated frontend CSS to Tailwind v4 - Reverted socket IO origin restriction - Fixed search queries not persisting after auth redirect - Move advanced search options to left UI selector - Unlock IRC source to be used for audiobook content_type - Tweaked security settings env var syncing to be prioritised - Fix AA "all languages" query generation - Added language-free AA query as second fallback in case of no results - Testing moving SeleniumBase scratch files to /tmp via symlink - Added enhanced logging for activity dismissals and other events - Removed iFrame restrictions
196 lines
6.4 KiB
TypeScript
196 lines
6.4 KiB
TypeScript
import { CSSProperties } from 'react';
|
|
import { Book, ButtonStateInfo } from '../types';
|
|
import { CircularProgress } from './shared';
|
|
|
|
type ButtonSize = 'sm' | 'md';
|
|
type ButtonVariant = 'default' | 'icon';
|
|
|
|
interface BookGetButtonProps {
|
|
book: Book;
|
|
onGetReleases: (book: Book) => void;
|
|
buttonState?: ButtonStateInfo;
|
|
isLoading?: boolean;
|
|
size?: ButtonSize;
|
|
variant?: ButtonVariant;
|
|
fullWidth?: boolean;
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
sm: 'px-2.5 py-1.5 text-xs',
|
|
md: 'px-4 py-2.5 text-sm',
|
|
};
|
|
|
|
const iconSizeClasses: Record<ButtonSize, string> = {
|
|
sm: 'p-1.5',
|
|
md: 'p-1.5 sm:p-2',
|
|
};
|
|
|
|
const iconSizes: Record<ButtonSize, string> = {
|
|
sm: 'w-3.5 h-3.5',
|
|
md: 'w-4 h-4',
|
|
};
|
|
|
|
const iconOnlySizes: Record<ButtonSize, string> = {
|
|
sm: 'w-4 h-4',
|
|
md: 'w-4 h-4 sm:w-5 sm:h-5',
|
|
};
|
|
|
|
export const BookGetButton = ({
|
|
book,
|
|
onGetReleases,
|
|
buttonState,
|
|
isLoading = false,
|
|
size = 'md',
|
|
variant = 'default',
|
|
fullWidth = false,
|
|
className = '',
|
|
style,
|
|
}: BookGetButtonProps) => {
|
|
const isIconVariant = variant === 'icon';
|
|
const widthClasses = fullWidth ? 'w-full' : '';
|
|
const sizeClass = isIconVariant ? iconSizeClasses[size] : sizeClasses[size];
|
|
const iconSize = isIconVariant ? iconOnlySizes[size] : iconSizes[size];
|
|
|
|
// Determine states based on buttonState
|
|
const isCompleted = buttonState?.state === 'complete';
|
|
const hasError = buttonState?.state === 'error';
|
|
const isBlocked = buttonState?.state === 'blocked';
|
|
const isInProgress = buttonState && ['queued', 'resolving', 'locating', 'downloading'].includes(buttonState.state);
|
|
const showCircularProgress = buttonState?.state === 'downloading' && buttonState.progress !== undefined;
|
|
const showSpinner = (isInProgress && !showCircularProgress) || isLoading;
|
|
|
|
// Disable button while loading metadata
|
|
const isDisabled = isLoading || isBlocked;
|
|
|
|
// Determine button styling based on state
|
|
const getButtonClasses = () => {
|
|
if (isCompleted) {
|
|
return isIconVariant
|
|
? 'bg-green-600 text-white'
|
|
: 'bg-green-600 hover:bg-green-700';
|
|
}
|
|
if (hasError) {
|
|
return isIconVariant
|
|
? 'bg-red-600 text-white opacity-75'
|
|
: 'bg-red-600 hover:bg-red-700';
|
|
}
|
|
if (isBlocked) {
|
|
return isIconVariant
|
|
? 'text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-70'
|
|
: 'bg-gray-500 opacity-75 cursor-not-allowed';
|
|
}
|
|
if (isLoading) {
|
|
// Show loading state (fetching metadata)
|
|
return isIconVariant
|
|
? 'text-gray-400 dark:text-gray-500'
|
|
: 'bg-emerald-600/70';
|
|
}
|
|
if (isInProgress) {
|
|
// Show progress state but keep it clickable
|
|
return isIconVariant
|
|
? 'bg-sky-600 text-white'
|
|
: 'bg-sky-600 hover:bg-sky-700';
|
|
}
|
|
// Default state - icon variant has no background
|
|
return isIconVariant
|
|
? 'text-gray-600 dark:text-gray-200 hover-action'
|
|
: 'bg-emerald-600 hover:bg-emerald-700';
|
|
};
|
|
|
|
const handleClick = () => {
|
|
if (isDisabled) return;
|
|
onGetReleases(book);
|
|
};
|
|
|
|
// Determine display text
|
|
const getDisplayText = () => {
|
|
if (isBlocked) return buttonState?.text || 'Unavailable';
|
|
if (isCompleted) return 'Downloaded';
|
|
if (hasError) return 'Failed';
|
|
if (isLoading) return 'Loading';
|
|
if (buttonState?.state === 'downloading') return 'Downloading';
|
|
if (buttonState?.state === 'locating') return 'Locating files';
|
|
if (buttonState?.state === 'resolving') return 'Resolving';
|
|
if (buttonState?.state === 'queued') return 'Queued';
|
|
if (buttonState?.state === 'download' && buttonState.text) return buttonState.text;
|
|
return 'Get';
|
|
};
|
|
|
|
// Render appropriate icon based on state
|
|
const renderIcon = () => {
|
|
if (isCompleted) {
|
|
return (
|
|
<svg className={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
if (hasError) {
|
|
return (
|
|
<svg className={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
if (isBlocked) {
|
|
return (
|
|
<svg className={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16.5 10.5V7.875a4.125 4.125 0 1 0-8.25 0V10.5m-.75 0h9a2.25 2.25 0 0 1 2.25 2.25v6A2.25 2.25 0 0 1 16.5 21h-9a2.25 2.25 0 0 1-2.25-2.25v-6a2.25 2.25 0 0 1 2.25-2.25Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
if (showCircularProgress) {
|
|
const progressSize = isIconVariant ? (size === 'sm' ? 16 : 20) : (size === 'sm' ? 12 : 16);
|
|
return <CircularProgress progress={buttonState?.progress} size={progressSize} />;
|
|
}
|
|
|
|
if (showSpinner) {
|
|
return (
|
|
<div
|
|
className={`${iconSize} border-2 border-current border-t-transparent rounded-full animate-spin`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Default "+" icon for Get action
|
|
return (
|
|
<svg className={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
// Icon variant renders as a circular button without text
|
|
if (isIconVariant) {
|
|
return (
|
|
<button
|
|
className={`flex items-center justify-center rounded-full transition-all duration-200 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-emerald-500 ${sizeClass} ${getButtonClasses()} ${className}`.trim()}
|
|
onClick={handleClick}
|
|
disabled={isDisabled}
|
|
style={style}
|
|
aria-label={`${getDisplayText()} releases for ${book.title}`}
|
|
>
|
|
{renderIcon()}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button
|
|
className={`inline-flex items-center justify-center gap-1.5 rounded-sm text-white transition-all duration-200 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-emerald-500 ${sizeClass} ${widthClasses} ${getButtonClasses()} ${className}`.trim()}
|
|
onClick={handleClick}
|
|
disabled={isDisabled}
|
|
style={style}
|
|
aria-label={`${getDisplayText()} releases for ${book.title}`}
|
|
>
|
|
{renderIcon()}
|
|
<span>{getDisplayText()}</span>
|
|
</button>
|
|
);
|
|
};
|