Remove unused components and models for improved maintainability

- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files.
- Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter.
- Updated project structure and references to reflect these removals.
This commit is contained in:
natreex
2026-03-22 22:37:31 -04:00
parent e8aaef108b
commit 64ed90d993
229 changed files with 15091 additions and 21289 deletions

View File

@@ -1,20 +0,0 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPlus} from "@fortawesome/free-solid-svg-icons";
import React from "react";
interface AddActionButtonProps {
callBackAction: () => Promise<void>;
}
export default function AddActionButton(
{
callBackAction
}: AddActionButtonProps) {
return (
<button
className={`group p-2 rounded-lg text-muted hover:text-primary hover:bg-primary/10 transition-colors`}
onClick={callBackAction}>
<FontAwesomeIcon icon={faPlus} className={'w-5 h-5 transition-transform group-hover:rotate-90'}/>
</button>
)
}

View File

@@ -1,61 +1,48 @@
'use client'
import React from "react";
import {faBrain, faTriangleExclamation} from "@fortawesome/free-solid-svg-icons";
import InputField from "@/components/form/InputField";
import ToggleWithConfirmation from "@/components/form/ToggleWithConfirmation";
import {useTranslations} from "next-intl";
interface AdvancedGenerationOptionsProps {
useExplicit: boolean;
setUseExplicit: (value: boolean) => void;
useSmart: boolean;
setUseSmart: (value: boolean) => void;
}
export default function AdvancedGenerationOptions({
useExplicit,
setUseExplicit,
useSmart,
setUseSmart
}: AdvancedGenerationOptionsProps) {
const t = useTranslations();
return (
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<div className="flex justify-evenly items-center">
<InputField
icon={faTriangleExclamation}
fieldName={t("generationOptions.explicit.label")}
centered
input={
<ToggleWithConfirmation
checked={useExplicit}
onChange={setUseExplicit}
alertTitle={t("generationOptions.explicit.alertTitle")}
alertMessage={t("generationOptions.explicit.alertMessage")}
alertType="alert"
confirmText={t("generationOptions.activate")}
cancelText={t("generationOptions.cancel")}
/>
}
/>
<InputField
icon={faBrain}
fieldName={t("generationOptions.smart.label")}
centered
input={
<ToggleWithConfirmation
checked={useSmart}
onChange={setUseSmart}
alertTitle={t("generationOptions.smart.alertTitle")}
alertMessage={t("generationOptions.smart.alertMessage")}
alertType="informatif"
confirmText={t("generationOptions.activate")}
cancelText={t("generationOptions.cancel")}
/>
}
/>
</div>
</div>
);
}
'use client'
import React from "react";
import {AlertTriangle, Brain} from "lucide-react";
import ToggleField from "@/components/form/ToggleField";
import {useTranslations} from '@/lib/i18n';
interface AdvancedGenerationOptionsProps {
useExplicit: boolean;
setUseExplicit: (value: boolean) => void;
useSmart: boolean;
setUseSmart: (value: boolean) => void;
}
export default function AdvancedGenerationOptions({
useExplicit,
setUseExplicit,
useSmart,
setUseSmart
}: AdvancedGenerationOptionsProps) {
const t = useTranslations();
return (
<div className="flex justify-evenly items-center">
<ToggleField
icon={AlertTriangle}
label={t("generationOptions.explicit.label")}
checked={useExplicit}
onChange={setUseExplicit}
alertTitle={t("generationOptions.explicit.alertTitle")}
alertMessage={t("generationOptions.explicit.alertMessage")}
alertType="alert"
confirmText={t("generationOptions.activate")}
cancelText={t("generationOptions.cancel")}
/>
<ToggleField
icon={Brain}
label={t("generationOptions.smart.label")}
checked={useSmart}
onChange={setUseSmart}
alertTitle={t("generationOptions.smart.alertTitle")}
alertMessage={t("generationOptions.smart.alertMessage")}
alertType="informatif"
confirmText={t("generationOptions.activate")}
cancelText={t("generationOptions.cancel")}
/>
</div>
);
}

View File

@@ -1,21 +0,0 @@
import React from "react";
interface CancelButtonProps {
callBackFunction: () => void;
text?: string;
}
export default function CancelButton(
{
callBackFunction,
text = "Annuler"
}: CancelButtonProps) {
return (
<button
onClick={callBackFunction}
className="px-5 py-2.5 rounded-lg bg-secondary/50 text-text-primary border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md transition-all duration-200 hover:scale-105 font-medium"
>
{text}
</button>
);
}

View File

@@ -1,4 +1,4 @@
import {Dispatch, SetStateAction} from "react";
import React, {Dispatch, SetStateAction} from "react";
interface CheckBoxProps {
isChecked: boolean;
@@ -27,15 +27,15 @@ export default function CheckBox(
className="hidden"
/>
<label htmlFor={id}
className={`block overflow-hidden h-6 rounded-full cursor-pointer transition-all duration-200 border-2 shadow-sm hover:shadow-md ${
className={`block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-150 border-2 ${
isChecked
? 'bg-primary border-primary shadow-primary/30'
: 'bg-secondary/50 border-secondary hover:bg-secondary'
? 'bg-primary border-primary'
: 'bg-secondary border-secondary hover:bg-secondary'
}`}
>
<span
className={`absolute block h-5 w-5 rounded-full bg-white shadow-md transform transition-all duration-200 top-0.5 ${
isChecked ? 'right-0.5 scale-110' : 'left-0.5'
className={`absolute block h-5 w-5 rounded-full bg-text-primary transform transition-all duration-200 top-0.5 ${
isChecked ? 'right-0.5' : 'left-0.5'
}`}/>
</label>
</div>

View File

@@ -1,40 +0,0 @@
import React from "react";
type ButtonType = 'alert' | 'danger' | 'informatif' | 'success';
interface ConfirmButtonProps {
text: string;
callBackFunction?: () => void;
buttonType?: ButtonType;
}
export default function ConfirmButton(
{
text,
callBackFunction,
buttonType = 'success'
}: ConfirmButtonProps) {
function getButtonType(alertType: ButtonType): string {
switch (alertType) {
case 'alert':
return 'bg-warning';
case 'danger':
return 'bg-error';
case 'informatif':
return 'bg-info';
case 'success':
default:
return 'bg-success';
}
}
const applyType: string = getButtonType(buttonType);
return (
<button
onClick={callBackFunction}
className={`rounded-lg ${applyType} px-5 py-2.5 text-white font-semibold shadow-md hover:shadow-lg transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-4 focus:ring-primary/20`}
>
{text}
</button>
)
}

View File

@@ -1,4 +1,4 @@
import {ChangeEvent} from "react";
import React, {ChangeEvent} from "react";
interface DatePickerProps {
date: string;
@@ -15,7 +15,7 @@ export default function DatePicker(
type="date"
value={date}
onChange={setDate}
className="bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:bg-secondary hover:border-secondary outline-none transition-all duration-200"
className="input-base"
/>
)
}

View File

@@ -1,66 +0,0 @@
'use client';
import {useState} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faTrash} from '@fortawesome/free-solid-svg-icons';
import AlertBox from '@/components/AlertBox';
interface DeleteButtonProps {
onDelete: () => void | Promise<void>;
confirmTitle: string;
confirmMessage: string;
confirmButtonText: string;
cancelButtonText: string;
disabled?: boolean;
className?: string;
}
export default function DeleteButton(
{
onDelete,
confirmTitle,
confirmMessage,
confirmButtonText,
cancelButtonText,
disabled = false,
className = ''
}: DeleteButtonProps
) {
const [showConfirm, setShowConfirm] = useState<boolean>(false);
function handlePress(): void {
if (disabled) return;
setShowConfirm(true);
}
async function handleConfirm(): Promise<void> {
setShowConfirm(false);
await onDelete();
}
function handleCancel(): void {
setShowConfirm(false);
}
return (
<>
<button
onClick={handlePress}
disabled={disabled}
className={`flex items-center justify-center bg-error/90 hover:bg-error w-10 h-10 rounded-xl border border-error shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200 ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
>
<FontAwesomeIcon icon={faTrash} className="text-text-primary w-5 h-5"/>
</button>
{showConfirm && (
<AlertBox
title={confirmTitle}
message={confirmMessage}
type="danger"
confirmText={confirmButtonText}
cancelText={cancelButtonText}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
</>
);
}

View File

@@ -1,5 +1,6 @@
import {ChangeEvent, KeyboardEvent, useRef, useState} from "react";
import AddActionButton from "@/components/form/AddActionButton";
import React, {ChangeEvent, KeyboardEvent, useRef, useState} from "react";
import IconButton from "@/components/ui/IconButton";
import {Plus} from "lucide-react";
interface InlineAddInputProps {
value: string;
@@ -37,11 +38,11 @@ export default function InlineAddInput(
setIsAdding(false);
}
}}
className="relative flex items-center gap-1 h-[44px] px-3 bg-secondary/30 rounded-xl border-2 border-dashed border-secondary/50 hover:border-primary/60 hover:bg-secondary/50 cursor-pointer transition-colors duration-200"
className="relative flex items-center gap-1 h-[44px] px-3 bg-dark-background rounded-xl border border-secondary hover:border-primary/60 cursor-pointer transition-colors duration-200"
>
{showNumericalInput && numericalValue !== undefined && setNumericalValue && (
<input
className={`bg-secondary/50 text-primary text-sm px-1.5 py-0.5 rounded border border-secondary/50 transition-all duration-200 !outline-none !ring-0 !shadow-none focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!shadow-none ${isAdding ? 'w-10 opacity-100' : 'w-0 opacity-0 px-0 border-0'}`}
className={`bg-dark-background text-primary text-sm px-1.5 py-0.5 rounded border border-secondary transition-all duration-200 !outline-none !ring-0 !shadow-none focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!shadow-none ${isAdding ? 'w-10 opacity-100' : 'w-0 opacity-0 px-0 border-0'}`}
type="number"
value={numericalValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => setNumericalValue(parseInt(e.target.value))}
@@ -63,7 +64,7 @@ export default function InlineAddInput(
/>
{isAdding && (
<div className="absolute right-1 opacity-100">
<AddActionButton callBackAction={handleAdd}/>
<IconButton icon={Plus} variant="ghost" onClick={handleAdd}/>
</div>
)}
</li>

View File

@@ -1,20 +1,21 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {faPlus, faTrash} from "@fortawesome/free-solid-svg-icons";
import {LucideIcon, Plus, Trash2} from "lucide-react";
import IconButton from "@/components/ui/IconButton";
import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
interface InputFieldProps {
icon?: IconDefinition,
fieldName?: string,
input: React.ReactNode,
addButtonCallBack?: () => Promise<void>
removeButtonCallBack?: () => Promise<void>
isAddButtonDisabled?: boolean
action?: () => Promise<void>
actionLabel?: string
actionIcon?: IconDefinition
hint?: string,
centered?: boolean,
icon?: LucideIcon;
fieldName?: string;
input: React.ReactNode;
addButtonCallBack?: () => Promise<void>;
removeButtonCallBack?: () => Promise<void>;
isAddButtonDisabled?: boolean;
action?: () => Promise<void>;
actionLabel?: string;
actionIcon?: LucideIcon;
hint?: string;
centered?: boolean;
}
export default function InputField(
@@ -31,63 +32,51 @@ export default function InputField(
hint,
centered = false
}: InputFieldProps) {
function renderIcon(): React.JSX.Element | null {
if (!icon) return null;
const Icon: LucideIcon = icon;
return <Icon className="text-primary w-5 h-5" strokeWidth={1.75}/>;
}
return (
<div className={`flex flex-col ${centered ? 'items-center' : ''}`}>
<div className={`flex items-center mb-2 lg:mb-3 flex-wrap gap-2 ${centered ? 'justify-center' : 'justify-between'}`}>
{
fieldName && (
<h3 className="text-text-primary text-xl font-[ADLaM Display] font-medium mb-2 flex items-center gap-2">
{
icon && <FontAwesomeIcon icon={icon} className="text-primary w-5 h-5"/>
}
{fieldName}
</h3>
)
}
{
action && (
<button
onClick={action}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-secondary/50 rounded-lg text-primary hover:bg-secondary hover:shadow-md hover:scale-105 transition-all duration-200 border border-secondary/50 font-medium"
>
{
actionIcon && <FontAwesomeIcon icon={actionIcon} className={'w-3.5 h-3.5'}/>
}
{
actionLabel && <span>{actionLabel}</span>
}
</button>
)
}
<div
className={`flex items-center mb-2 lg:mb-3 flex-wrap gap-2 ${centered ? 'justify-center' : 'justify-between'}`}>
{fieldName && (
<h3 className="text-text-secondary text-sm font-medium flex items-center gap-2">
{renderIcon()}
{fieldName}
</h3>
)}
{action && actionIcon && (
<Button variant="secondary" size="sm" icon={actionIcon} onClick={action}>
{actionLabel && <span>{actionLabel}</span>}
</Button>
)}
{hint && (
<span
className="text-xs text-muted bg-secondary/30 px-3 py-1.5 rounded-lg border border-secondary/30">
{hint}
</span>
<Badge variant="muted" size="sm">{hint}</Badge>
)}
</div>
<div className={`flex items-center gap-2 ${centered ? 'justify-center' : 'justify-between'}`}>
{input}
{
addButtonCallBack && (
<button
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 hover:bg-primary-dark hover:shadow-lg hover:scale-110 shadow-md"
onClick={addButtonCallBack}
disabled={isAddButtonDisabled}>
<FontAwesomeIcon icon={faPlus} className="w-4 h-4"/>
</button>
)
}
{
removeButtonCallBack && (
<button
className="bg-error/90 hover:bg-error text-text-primary w-9 h-9 rounded-full flex items-center justify-center transition-all duration-200 hover:shadow-lg hover:scale-110 shadow-md"
onClick={removeButtonCallBack}
>
<FontAwesomeIcon icon={faTrash} className={'w-4 h-4'}/>
</button>
)
}
{addButtonCallBack && (
<IconButton
icon={Plus}
variant="ghost"
shape="square"
onClick={addButtonCallBack}
disabled={isAddButtonDisabled}
/>
)}
{removeButtonCallBack && (
<IconButton
icon={Trash2}
variant="danger"
shape="square"
onClick={removeButtonCallBack}
/>
)}
</div>
</div>
)

View File

@@ -42,13 +42,7 @@ export default function NumberInput(
type="number"
value={value ?? ''}
onChange={handleChange}
className={`w-full bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50
focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary
hover:bg-secondary hover:border-secondary
placeholder:text-muted/60
outline-none transition-all duration-200
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${readOnly ? 'cursor-default' : ''}`}
className="input-base"
placeholder={placeholder}
readOnly={readOnly}
disabled={disabled}

View File

@@ -1,7 +1,6 @@
import {storyStates} from "@/lib/models/Story";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBookOpen, faKeyboard, faMagicWandSparkles, faPalette, faPenNib} from "@fortawesome/free-solid-svg-icons";
import {Dispatch, SetStateAction} from "react";
import {storyStates} from "@/lib/constants/story";
import {BookOpen, Keyboard, LucideIcon, Palette, PenLine, Wand2} from "lucide-react";
import React, {Dispatch, SetStateAction} from "react";
export interface RadioBoxValue {
label: string;
@@ -14,6 +13,8 @@ interface RadioBoxProps {
name: string;
}
const storyStateIcons: LucideIcon[] = [PenLine, Keyboard, Palette, BookOpen, Wand2];
export default function RadioBox(
{
selected,
@@ -22,41 +23,36 @@ export default function RadioBox(
}: RadioBoxProps) {
return (
<div className="flex flex-wrap gap-2">
{storyStates.map((option: RadioBoxValue) => (
<div key={option.value} className="flex items-center">
<input
type="radio"
id={option.label}
name={name}
value={option.value}
checked={selected === option.value}
onChange={() => setSelected(option.value)}
className="hidden"
/>
<label
htmlFor={option.label}
className={`px-3 lg:px-4 py-2 lg:py-2.5 text-xs lg:text-sm font-medium rounded-xl cursor-pointer transition-all duration-200 flex items-center gap-2 ${
selected === option.value
? 'bg-primary text-text-primary shadow-lg shadow-primary/30 scale-105 border border-primary-dark'
: 'bg-secondary/50 text-muted hover:bg-secondary hover:text-text-primary hover:scale-105 border border-secondary/50 hover:border-secondary'
}`}
>
<FontAwesomeIcon
icon={
[
faPenNib,
faKeyboard,
faPalette,
faBookOpen,
faMagicWandSparkles
][option.value]
}
className={selected === option.value ? "text-text-primary w-5 h-5" : "text-muted w-5 h-5"}
{storyStates.map((option: RadioBoxValue) => {
const Icon = storyStateIcons[option.value];
return (
<div key={option.value} className="flex items-center">
<input
type="radio"
id={option.label}
name={name}
value={option.value}
checked={selected === option.value}
onChange={() => setSelected(option.value)}
className="hidden"
/>
{option.label}
</label>
</div>
))}
<label
htmlFor={option.label}
className={`px-3 lg:px-4 py-2 lg:py-2.5 text-xs lg:text-sm font-medium rounded-xl cursor-pointer transition-colors duration-150 flex items-center gap-2 ${
selected === option.value
? 'bg-secondary text-primary border border-primary'
: 'bg-secondary text-text-secondary hover:text-text-primary border border-secondary'
}`}
>
<Icon
className={selected === option.value ? "text-text-primary w-5 h-5" : "text-muted w-5 h-5"}
strokeWidth={1.75}
/>
{option.label}
</label>
</div>
);
})}
</div>
)
}

View File

@@ -36,10 +36,10 @@ export default function RadioGroup(
/>
<label
htmlFor={`${name}-${option.value}`}
className={`px-4 py-2 rounded-lg cursor-pointer transition-all duration-200 text-sm font-medium border ${
className={`px-4 py-2 rounded-lg cursor-pointer transition-colors duration-150 text-sm font-medium border ${
value === option.value
? 'bg-primary/20 text-primary border-primary/40 shadow-md'
: 'bg-secondary/30 text-text-primary border-secondary/50 hover:bg-secondary hover:border-secondary hover:scale-105'
? 'bg-secondary text-primary border-primary'
: 'bg-secondary text-text-secondary border-secondary hover:text-text-primary'
}`}
>
{option.label}

View File

@@ -1,7 +1,7 @@
import {ChangeEvent} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {SelectBoxProps} from "@/shared/interface";
import React, {ChangeEvent} from "react";
import {LucideIcon} from "lucide-react";
import {SelectBoxProps} from "@/components/form/SelectBox";
import IconButton from "@/components/ui/IconButton";
interface SearchInputWithSelectProps {
selectValue: string;
@@ -10,7 +10,7 @@ interface SearchInputWithSelectProps {
inputValue: string;
setInputValue: (value: string) => void;
inputPlaceholder?: string;
searchIcon: IconDefinition;
searchIcon: LucideIcon;
onSearch: () => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
@@ -32,7 +32,7 @@ export default function SearchInputWithSelect(
<select
value={selectValue}
onChange={(e: ChangeEvent<HTMLSelectElement>) => setSelectValue(e.target.value)}
className="bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:bg-secondary hover:border-secondary outline-none transition-all duration-200 font-medium"
className="input-base cursor-pointer font-medium w-auto"
>
{selectOptions.map((option: SelectBoxProps) => (
<option key={option.value} value={option.value}>
@@ -40,22 +40,19 @@ export default function SearchInputWithSelect(
</option>
))}
</select>
<div className="flex-1 relative">
<input
type="text"
value={inputValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
placeholder={inputPlaceholder}
className="w-full bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:bg-secondary hover:border-secondary placeholder:text-muted/60 outline-none transition-all duration-200 pr-12"
className="input-base pr-12"
onKeyDown={onKeyDown}
/>
<button
onClick={onSearch}
className="absolute right-0 top-0 h-full px-4 text-primary hover:text-primary-light hover:scale-110 transition-all duration-200"
>
<FontAwesomeIcon icon={searchIcon} className="w-5 h-5"/>
</button>
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<IconButton icon={searchIcon} variant="ghost" onClick={onSearch}/>
</div>
</div>
</div>
);

View File

@@ -1,12 +1,26 @@
import {ChangeEvent} from "react";
import {SelectBoxProps} from "@/shared/interface";
import React, {ChangeEvent} from "react";
import {useTranslations} from '@/lib/i18n';
export interface SelectBoxProps {
label: string;
value: string;
}
type InputSize = 'sm' | 'md' | 'lg';
const sizeClasses: Record<InputSize, string> = {
sm: 'px-3 py-1.5 text-xs rounded-lg',
md: 'px-4 py-2.5 text-sm rounded-xl',
lg: 'px-5 py-3 text-base rounded-xl',
};
export interface SelectBoxFormProps {
onChangeCallBack: (event: ChangeEvent<HTMLSelectElement>) => void,
data: SelectBoxProps[],
defaultValue: string | null | undefined,
placeholder?: string,
disabled?: boolean
disabled?: boolean,
size?: InputSize,
translate?: boolean
}
export default function SelectBox(
@@ -15,26 +29,24 @@ export default function SelectBox(
data,
defaultValue,
placeholder,
disabled
disabled,
size = 'md',
translate = false
}: SelectBoxFormProps) {
const t = useTranslations();
return (
<select
onChange={onChangeCallBack}
disabled={disabled}
key={defaultValue || 'placeholder'}
defaultValue={defaultValue || '0'}
className={`w-full text-text-primary bg-secondary/50 hover:bg-secondary px-4 py-2.5 rounded-xl
border border-secondary/50
focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary
hover:border-secondary
outline-none transition-all duration-200 cursor-pointer
${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
className={`input-base ${sizeClasses[size]}`}
>
{placeholder && <option value={'0'}>{placeholder}</option>}
{
data.map((item: SelectBoxProps) => (
<option key={item.value} value={item.value} className="bg-tertiary text-text-primary">
{item.label}
{translate ? t(item.label) : item.label}
</option>
))
}

View File

@@ -1,9 +1,9 @@
'use client'
import React, {useState} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faDownload, faSpinner} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {Download} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import SelectBox from '@/components/form/SelectBox';
import InputField from '@/components/form/InputField';
interface SeriesImportItem {
id: string;
@@ -18,19 +18,19 @@ interface SeriesImportSelectorProps {
}
export default function SeriesImportSelector({
availableItems,
onImport,
placeholder,
label
}: SeriesImportSelectorProps) {
availableItems,
onImport,
placeholder,
label
}: SeriesImportSelectorProps) {
const t = useTranslations();
const [selectedId, setSelectedId] = useState<string>('');
const [isImporting, setIsImporting] = useState<boolean>(false);
async function handleImport(): Promise<void> {
if (!selectedId || isImporting) return;
setIsImporting(true);
try {
await onImport(selectedId);
@@ -39,50 +39,36 @@ export default function SeriesImportSelector({
setIsImporting(false);
}
}
if (availableItems.length === 0) {
return null;
}
const selectData = availableItems.map((item) => ({
label: item.name,
value: item.id
}));
return (
<div className="bg-primary/10 border border-primary/30 rounded-xl p-4 mb-4">
{label && (
<h4 className="text-sm font-medium text-primary mb-3 flex items-center gap-2">
<FontAwesomeIcon icon={faDownload} className="w-4 h-4"/>
<Download className="w-4 h-4" strokeWidth={1.75}/>
{label}
</h4>
)}
<div className="flex items-center gap-3">
<div className="flex-grow">
<InputField
input={
<SelectBox
onChangeCallBack={(e) => setSelectedId(e.target.value)}
data={selectData}
defaultValue={selectedId}
placeholder={placeholder}
/>
</div>
<button
onClick={handleImport}
disabled={!selectedId || isImporting}
className={`px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all duration-200 ${
selectedId && !isImporting
? 'bg-primary text-white hover:bg-primary-dark hover:scale-105'
: 'bg-secondary/50 text-muted cursor-not-allowed'
}`}
>
{isImporting ? (
<FontAwesomeIcon icon={faSpinner} className="w-4 h-4 animate-spin"/>
) : (
<FontAwesomeIcon icon={faDownload} className="w-4 h-4"/>
)}
{t('seriesImport.importButton')}
</button>
</div>
}
addButtonCallBack={handleImport}
isAddButtonDisabled={!selectedId || isImporting}
/>
</div>
);
}

View File

@@ -1,55 +0,0 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
interface SubmitButtonWLoadingProps {
callBackAction: () => Promise<void> | void;
isLoading: boolean;
text: string;
loadingText: string;
icon?: IconDefinition;
}
export default function SubmitButtonWLoading(
{
callBackAction,
isLoading,
icon,
text,
loadingText
}: SubmitButtonWLoadingProps) {
return (
<button
onClick={callBackAction}
disabled={isLoading}
className={`group py-2.5 px-5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 relative overflow-hidden ${
isLoading
? 'bg-secondary cursor-not-allowed opacity-75'
: 'bg-secondary/80 hover:bg-secondary shadow-md hover:shadow-lg hover:shadow-primary/20 hover:scale-105 border border-secondary/50 hover:border-primary/30'
}`}
>
<span
className={`flex items-center gap-2 transition-all duration-200 ${isLoading ? 'opacity-0' : 'opacity-100'} text-primary`}>
{
icon &&
<FontAwesomeIcon icon={icon} className={'w-4 h-4 transition-transform group-hover:scale-110'}/>
}
<span className="text-sm">{text}</span>
</span>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-secondary/50 backdrop-blur-sm">
<FontAwesomeIcon icon={faSpinner} className="w-4 h-4 text-primary animate-spin"/>
<span className="ml-3 text-primary text-sm font-medium">
<span className="hidden sm:inline">
{loadingText}
</span>
<span className="sm:hidden">
{loadingText}
</span>
</span>
</div>
)}
</button>
)
}

View File

@@ -1,11 +1,13 @@
import {SelectBoxProps} from "@/shared/interface";
import {ChangeEvent, Dispatch, SetStateAction} from "react";
import {SelectBoxProps} from "@/components/form/SelectBox";
import React, {ChangeEvent, Dispatch, SetStateAction} from "react";
import {LucideIcon} from "lucide-react";
import InputField from "@/components/form/InputField";
import TextInput from "@/components/form/TextInput";
import Badge from "@/components/ui/Badge";
interface SuggestFieldInputProps {
inputFieldName: string;
inputFieldIcon?: any;
inputFieldIcon?: LucideIcon;
searchTags: string;
tagued: string[];
handleTagSearch: (e: ChangeEvent<HTMLInputElement>) => void;
@@ -32,7 +34,7 @@ export default function SuggestFieldInput(
getTagLabel
}: SuggestFieldInputProps) {
return (
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<div>
<InputField fieldName={inputFieldName} icon={inputFieldIcon} input={
<div className="w-full mb-3 relative">
<TextInput value={searchTags} setValue={handleTagSearch}
@@ -40,11 +42,11 @@ export default function SuggestFieldInput(
placeholder="Rechercher et ajouter..."/>
{showTagSuggestions && filteredTags().length > 0 && (
<div
className="absolute top-full left-0 right-0 z-10 mt-2 bg-tertiary border border-secondary/50 rounded-xl shadow-2xl max-h-48 overflow-y-auto backdrop-blur-sm">
className="absolute top-full left-0 right-0 z-10 mt-2 bg-tertiary rounded-xl max-h-48 overflow-y-auto">
{filteredTags().map((character: SelectBoxProps) => (
<button
key={character.value}
className="w-full text-left px-4 py-2.5 hover:bg-secondary/70 text-text-primary transition-all hover:pl-5 first:rounded-t-xl last:rounded-b-xl font-medium"
className="w-full text-left px-4 py-2.5 hover:bg-secondary text-text-primary transition-colors duration-150 first:rounded-t-xl last:rounded-b-xl font-medium"
onClick={() => handleAddTag(character.value)}
>
{character.label}
@@ -59,18 +61,15 @@ export default function SuggestFieldInput(
<p className="text-text-secondary text-sm italic">Aucun élément ajouté</p>
) : (
tagued.map((tag: string) => (
<div
key={tag}
className="group bg-primary/90 text-white rounded-full px-4 py-1.5 text-sm font-medium flex items-center gap-2 shadow-md hover:shadow-lg hover:scale-105 transition-all border border-primary-dark"
>
<Badge key={tag} variant="primary" size="md" interactive>
<span>{getTagLabel(tag)}</span>
<button
onClick={() => handleRemoveTag(tag)}
className="w-5 h-5 flex items-center justify-center rounded-full hover:bg-white/20 transition-all group-hover:scale-110 text-base font-bold"
className="w-5 h-5 flex items-center justify-center rounded-full hover:bg-text-primary/20 transition-colors duration-150 text-base font-bold"
>
×
</button>
</div>
</Badge>
))
)}
</div>

View File

@@ -1,18 +1,12 @@
'use client'
import React, {ReactNode, useContext, useState} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faArrowDown, faArrowUp, faSpinner} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {SessionContext} from '@/context/SessionContext';
import {AlertContext} from '@/context/AlertContext';
import {ArrowDown, ArrowUp} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {LangContext, LangContextProps} from '@/context/LangContext';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {BookContext} from '@/context/BookContext';
import {SyncedBook} from '@/lib/models/SyncedBook';
import System from '@/lib/models/System';
import * as tauri from '@/lib/tauri';
import {apiPost} from '@/lib/api/client';
import IconButton from '@/components/ui/IconButton';
export type SyncElementType = 'character' | 'world' | 'location' | 'spell';
@@ -34,59 +28,42 @@ interface SeriesSyncUploadResponse {
}
export default function SyncFieldWrapper({
children,
seriesElementId,
seriesValue,
currentValue,
bookElementId,
field,
elementType,
onDownload,
onSyncComplete
}: SyncFieldWrapperProps) {
children,
seriesElementId,
seriesValue,
currentValue,
bookElementId,
field,
elementType,
onDownload,
onSyncComplete
}: SyncFieldWrapperProps) {
const t = useTranslations();
const {session} = useContext(SessionContext);
const {errorMessage, successMessage} = useContext(AlertContext);
const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const [isUploading, setIsUploading] = useState<boolean>(false);
const isLinkedToSeries: boolean = !!seriesElementId;
const hasSeriesDiff: boolean = isLinkedToSeries && seriesValue !== currentValue;
async function handleUpload(): Promise<void> {
if (!seriesElementId || isUploading) return;
setIsUploading(true);
try {
const requestData = {
type: elementType,
bookElementId: bookElementId,
field: field,
value: currentValue
};
let response: SeriesSyncUploadResponse;
if (isCurrentlyOffline() || book?.localBook) {
response = await tauri.seriesSyncUpload(requestData) as SeriesSyncUploadResponse;
} else {
response = await System.authPostToServer<SeriesSyncUploadResponse>(
'series/propagate',
requestData,
session.accessToken,
lang
);
if (book?.bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book.bookId)) {
addToQueue('series_sync_upload', {data: requestData});
}
}
const response: SeriesSyncUploadResponse = await apiPost<SeriesSyncUploadResponse>(
'series/propagate',
{
type: elementType,
bookElementId: bookElementId,
field: field,
value: currentValue
},
session.accessToken,
lang
);
if (response.success) {
successMessage(t('syncField.uploadSuccess', {count: response.updatedCount}));
if (onSyncComplete) {
@@ -101,50 +78,38 @@ export default function SyncFieldWrapper({
setIsUploading(false);
}
}
function handleDownload(): void {
onDownload();
}
if (!isLinkedToSeries) {
return <>{children}</>;
}
return (
<div className="flex items-start gap-2 w-full">
<button
<div className="flex items-center gap-2 w-full">
<IconButton
icon={ArrowDown}
variant={hasSeriesDiff ? 'primary' : 'muted'}
size="sm"
onClick={handleDownload}
disabled={!hasSeriesDiff}
title={t('syncField.downloadTooltip')}
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition-all duration-200 ${
hasSeriesDiff
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/40 hover:scale-110 cursor-pointer'
: 'bg-secondary/30 text-muted cursor-not-allowed opacity-50'
}`}
>
<FontAwesomeIcon icon={faArrowDown} className="w-3.5 h-3.5"/>
</button>
tooltip={t('syncField.downloadTooltip')}
/>
<div className="flex-grow">
{children}
</div>
<button
<IconButton
icon={ArrowUp}
variant={hasSeriesDiff && !isUploading ? 'primary' : 'muted'}
size="sm"
onClick={handleUpload}
disabled={isUploading || !hasSeriesDiff}
title={t('syncField.uploadTooltip')}
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition-all duration-200 ${
hasSeriesDiff && !isUploading
? 'bg-primary/20 text-primary hover:bg-primary/40 hover:scale-110 cursor-pointer'
: 'bg-secondary/30 text-muted cursor-not-allowed opacity-50'
}`}
>
{isUploading ? (
<FontAwesomeIcon icon={faSpinner} className="w-3.5 h-3.5 animate-spin"/>
) : (
<FontAwesomeIcon icon={faArrowUp} className="w-3.5 h-3.5"/>
)}
</button>
tooltip={t('syncField.uploadTooltip')}
/>
</div>
);
}

View File

@@ -1,4 +1,12 @@
import {ChangeEvent} from "react";
import React, {ChangeEvent} from "react";
type InputSize = 'sm' | 'md' | 'lg';
const sizeClasses: Record<InputSize, string> = {
sm: 'px-3 py-1.5 text-xs rounded-lg',
md: 'px-4 py-2.5 text-sm rounded-xl',
lg: 'px-5 py-3 text-base rounded-xl',
};
interface TextInputProps {
value: string;
@@ -7,6 +15,7 @@ interface TextInputProps {
readOnly?: boolean;
disabled?: boolean;
onFocus?: () => void;
size?: InputSize;
}
export default function TextInput(
@@ -16,7 +25,8 @@ export default function TextInput(
placeholder,
readOnly = false,
disabled = false,
onFocus
onFocus,
size = 'md'
}: TextInputProps) {
return (
<input
@@ -27,13 +37,7 @@ export default function TextInput(
disabled={disabled}
placeholder={placeholder}
onFocus={onFocus}
className={`w-full bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50
focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary
hover:bg-secondary hover:border-secondary
placeholder:text-muted/60
outline-none transition-all duration-200
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${readOnly ? 'cursor-default' : ''}`}
className={`input-base ${sizeClasses[size]}`}
/>
)
}

View File

@@ -1,117 +0,0 @@
import {ChangeEvent, useEffect, useState} from "react";
interface TextAreaInputProps {
value: string;
setValue: (e: ChangeEvent<HTMLTextAreaElement>) => void;
placeholder: string;
maxLength?: number;
}
export default function TextAreaInput(
{
value,
setValue,
placeholder,
maxLength
}: TextAreaInputProps) {
const [prevLength, setPrevLength] = useState(value.length);
const [isGrowing, setIsGrowing] = useState(false);
useEffect(() => {
if (value.length > prevLength) {
setIsGrowing(true);
setTimeout(() => setIsGrowing(false), 200);
}
setPrevLength(value.length);
}, [value.length, prevLength]);
const getProgressPercentage = () => {
if (!maxLength) return 0;
return Math.min((value.length / maxLength) * 100, 100);
};
const getStatusStyles = () => {
if (!maxLength) return {};
const percentage = getProgressPercentage();
if (percentage >= 100) return {
textColor: 'text-error',
bgColor: 'bg-error/10',
borderColor: 'border-error/30',
progressColor: 'bg-error'
};
if (percentage >= 90) return {
textColor: 'text-warning',
bgColor: 'bg-warning/10',
borderColor: 'border-warning/30',
progressColor: 'bg-warning'
};
if (percentage >= 75) return {
textColor: 'text-warning',
bgColor: 'bg-warning/10',
borderColor: 'border-warning/30',
progressColor: 'bg-warning'
};
return {
textColor: 'text-success',
bgColor: 'bg-success/10',
borderColor: 'border-success/30',
progressColor: 'bg-success'
};
};
const styles = getStatusStyles();
return (
<div className="flex-grow flex-col flex h-full">
<textarea
value={value}
onChange={setValue}
placeholder={placeholder}
rows={3}
className={`w-full flex-grow text-text-primary p-3 lg:p-4 rounded-xl border-2 outline-none resize-none transition-all duration-300 placeholder:text-muted/60 ${
maxLength && value.length >= maxLength
? 'border-error focus:ring-4 focus:ring-error/20 bg-error/10 hover:bg-error/15'
: 'bg-secondary/50 border-secondary/50 focus:ring-4 focus:ring-primary/20 focus:border-primary focus:bg-secondary hover:bg-secondary hover:border-secondary'
}`}
style={{height: '100%', minHeight: '200px'}}
/>
{maxLength && (
<div className="flex items-center justify-end gap-3 mt-3">
{/* Compteur avec effet de croissance */}
<div className={`flex items-center gap-3 px-4 py-2 rounded-lg border transition-all duration-300 ${
isGrowing ? 'scale-110 shadow-lg' : 'scale-100'
} ${styles.bgColor} ${styles.borderColor}`}>
{/* Progress bar visible */}
<div className="flex items-center gap-2">
<span className="text-xs text-muted font-medium">Progression</span>
<div className="w-20 h-2 bg-secondary/50 rounded-full overflow-hidden shadow-inner">
<div
className={`h-full rounded-full transition-all duration-500 ease-out ${styles.progressColor} shadow-md`}
style={{width: `${getProgressPercentage()}%`}}
></div>
</div>
</div>
{/* Compteur de caractères */}
<div className="flex items-center gap-2">
<div className={`text-sm font-semibold transition-all duration-200 ${
isGrowing ? 'scale-125' : 'scale-100'
} ${styles.textColor}`}>
{value.length}
<span className="text-muted mx-1">/</span>
<span className="text-text-secondary">{maxLength}</span>
</div>
<span className="text-xs text-muted font-medium">caractères</span>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -5,9 +5,16 @@ interface ToggleSwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
size?: 'sm' | 'md';
}
export default function ToggleSwitch({checked, onChange, disabled = false}: ToggleSwitchProps) {
export default function ToggleSwitch({checked, onChange, disabled = false, size = 'md'}: ToggleSwitchProps) {
const trackClass: string = size === 'sm' ? 'h-4 w-7' : 'h-6 w-11';
const thumbClass: string = size === 'sm' ? 'h-2.5 w-2.5' : 'h-4 w-4';
const translateClass: string = size === 'sm'
? (checked ? 'translate-x-3.5' : 'translate-x-0.5')
: (checked ? 'translate-x-6' : 'translate-x-1');
return (
<button
type="button"
@@ -15,14 +22,12 @@ export default function ToggleSwitch({checked, onChange, disabled = false}: Togg
aria-checked={checked}
disabled={disabled}
onClick={(): void => onChange(!checked)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
className={`relative inline-flex ${trackClass} items-center rounded-full transition-colors duration-200 ${
checked ? 'bg-primary' : 'bg-secondary'
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
checked ? 'translate-x-6' : 'translate-x-1'
}`}
className={`inline-block ${thumbClass} transform rounded-full bg-text-primary transition-transform duration-200 ${translateClass}`}
/>
</button>
);

View File

@@ -1,7 +1,7 @@
'use client'
import React, {useState} from "react";
import ToggleSwitch from "@/components/form/ToggleSwitch";
import AlertBox, {AlertType} from "@/components/AlertBox";
import AlertBox, {AlertType} from "@/components/ui/AlertBox";
interface ToggleWithConfirmationProps {
checked: boolean;
@@ -15,17 +15,17 @@ interface ToggleWithConfirmationProps {
}
export default function ToggleWithConfirmation({
checked,
onChange,
alertTitle,
alertMessage,
alertType,
confirmText = "Activer",
cancelText = "Annuler",
disabled = false
}: ToggleWithConfirmationProps) {
checked,
onChange,
alertTitle,
alertMessage,
alertType,
confirmText = "Activer",
cancelText = "Annuler",
disabled = false
}: ToggleWithConfirmationProps) {
const [showAlert, setShowAlert] = useState<boolean>(false);
function handleToggle(newChecked: boolean): void {
if (newChecked) {
setShowAlert(true);
@@ -33,20 +33,20 @@ export default function ToggleWithConfirmation({
onChange(false);
}
}
async function handleConfirm(): Promise<void> {
onChange(true);
setShowAlert(false);
}
function handleCancel(): void {
setShowAlert(false);
}
return (
<>
<ToggleSwitch checked={checked} onChange={handleToggle} disabled={disabled}/>
{showAlert && (
<AlertBox
title={alertTitle}