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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user