Add components for Act management and integrate Electron setup
This commit is contained in:
20
components/form/AddActionButton.tsx
Normal file
20
components/form/AddActionButton.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
21
components/form/CancelButton.tsx
Normal file
21
components/form/CancelButton.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
53
components/form/CheckBox.tsx
Normal file
53
components/form/CheckBox.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, {Dispatch, SetStateAction} from "react";
|
||||
|
||||
interface CheckBoxProps {
|
||||
isChecked: boolean;
|
||||
setIsChecked: Dispatch<SetStateAction<boolean>>;
|
||||
label: string;
|
||||
description: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function CheckBox(
|
||||
{
|
||||
isChecked,
|
||||
setIsChecked,
|
||||
label,
|
||||
description,
|
||||
id,
|
||||
}: CheckBoxProps) {
|
||||
return (
|
||||
<div className="flex items-center group">
|
||||
<div className="relative inline-block w-12 mr-3 align-middle select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={isChecked}
|
||||
onChange={() => setIsChecked(!isChecked)}
|
||||
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 ${
|
||||
isChecked
|
||||
? 'bg-primary border-primary shadow-primary/30'
|
||||
: 'bg-secondary/50 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'
|
||||
}`}/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label htmlFor={id}
|
||||
className="text-text-primary text-sm font-medium cursor-pointer group-hover:text-primary transition-colors">
|
||||
{label}
|
||||
</label>
|
||||
<p className="text-text-secondary text-xs mt-0.5 hidden md:block">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
components/form/ConfirmButton.tsx
Normal file
40
components/form/ConfirmButton.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
21
components/form/DatePicker.tsx
Normal file
21
components/form/DatePicker.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, {ChangeEvent} from "react";
|
||||
|
||||
interface DatePickerProps {
|
||||
date: string;
|
||||
setDate: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default function DatePicker(
|
||||
{
|
||||
setDate,
|
||||
date
|
||||
}: DatePickerProps) {
|
||||
return (
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
71
components/form/InlineAddInput.tsx
Normal file
71
components/form/InlineAddInput.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, {ChangeEvent, KeyboardEvent, useRef, useState} from "react";
|
||||
import AddActionButton from "@/components/form/AddActionButton";
|
||||
|
||||
interface InlineAddInputProps {
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
numericalValue?: number;
|
||||
setNumericalValue?: (value: number) => void;
|
||||
placeholder: string;
|
||||
onAdd: () => Promise<void>;
|
||||
showNumericalInput?: boolean;
|
||||
}
|
||||
|
||||
export default function InlineAddInput(
|
||||
{
|
||||
value,
|
||||
setValue,
|
||||
numericalValue,
|
||||
setNumericalValue,
|
||||
placeholder,
|
||||
onAdd,
|
||||
showNumericalInput = false
|
||||
}: InlineAddInputProps) {
|
||||
const [isAdding, setIsAdding] = useState<boolean>(false);
|
||||
const listItemRef = useRef<HTMLLIElement>(null);
|
||||
|
||||
async function handleAdd(): Promise<void> {
|
||||
await onAdd();
|
||||
setIsAdding(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={listItemRef}
|
||||
onBlur={(e: React.FocusEvent<HTMLLIElement, Element>): void => {
|
||||
if (!listItemRef.current?.contains(e.relatedTarget)) {
|
||||
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"
|
||||
>
|
||||
{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'}`}
|
||||
type="number"
|
||||
value={numericalValue}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setNumericalValue(parseInt(e.target.value))}
|
||||
tabIndex={isAdding ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
onFocus={(): void => setIsAdding(true)}
|
||||
onKeyUp={async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
await handleAdd();
|
||||
}
|
||||
}}
|
||||
className="flex-1 min-w-0 bg-transparent text-text-primary text-sm px-1 py-0.5 cursor-pointer focus:cursor-text placeholder:text-muted/60 transition-all duration-200 !outline-none !ring-0 !shadow-none focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!shadow-none"
|
||||
type="text"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{isAdding && (
|
||||
<div className="absolute right-1 opacity-100">
|
||||
<AddActionButton callBackAction={handleAdd}/>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
92
components/form/InputField.tsx
Normal file
92
components/form/InputField.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
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";
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
export default function InputField(
|
||||
{
|
||||
fieldName,
|
||||
icon,
|
||||
input,
|
||||
addButtonCallBack,
|
||||
removeButtonCallBack,
|
||||
isAddButtonDisabled,
|
||||
action,
|
||||
actionLabel,
|
||||
actionIcon,
|
||||
hint
|
||||
}: InputFieldProps) {
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'flex justify-between items-center mb-2 lg:mb-3 flex-wrap gap-2'}>
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
{hint && (
|
||||
<span
|
||||
className="text-xs text-muted bg-secondary/30 px-3 py-1.5 rounded-lg border border-secondary/30">
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
{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>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
components/form/NumberInput.tsx
Normal file
44
components/form/NumberInput.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, {ChangeEvent, Dispatch} from "react";
|
||||
|
||||
interface NumberInputProps {
|
||||
value: number;
|
||||
setValue: Dispatch<React.SetStateAction<number>>;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function NumberInput(
|
||||
{
|
||||
value,
|
||||
setValue,
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
disabled = false
|
||||
}: NumberInputProps
|
||||
) {
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
const newValue: number = parseInt(e.target.value);
|
||||
if (!isNaN(newValue)) {
|
||||
setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
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' : ''}`}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
62
components/form/RadioBox.tsx
Normal file
62
components/form/RadioBox.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
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 React, {Dispatch, SetStateAction} from "react";
|
||||
|
||||
export interface RadioBoxValue {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface RadioBoxProps {
|
||||
selected: number;
|
||||
setSelected: Dispatch<SetStateAction<number>>;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function RadioBox(
|
||||
{
|
||||
selected,
|
||||
setSelected,
|
||||
name
|
||||
}: 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"}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
components/form/RadioGroup.tsx
Normal file
51
components/form/RadioGroup.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
|
||||
interface RadioOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RadioGroupProps {
|
||||
name: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: RadioOption[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function RadioGroup(
|
||||
{
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
className = ""
|
||||
}: RadioGroupProps) {
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-3 ${className}`}>
|
||||
{options.map((option: RadioOption) => (
|
||||
<div key={option.value} className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={`${name}-${option.value}`}
|
||||
name={name}
|
||||
value={option.value}
|
||||
checked={value === option.value}
|
||||
onChange={() => onChange(option.value)}
|
||||
className="hidden"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${name}-${option.value}`}
|
||||
className={`px-4 py-2 rounded-lg cursor-pointer transition-all duration-200 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'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
components/form/SearchInputWithSelect.tsx
Normal file
62
components/form/SearchInputWithSelect.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, {ChangeEvent} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
|
||||
interface SearchInputWithSelectProps {
|
||||
selectValue: string;
|
||||
setSelectValue: (value: string) => void;
|
||||
selectOptions: SelectBoxProps[];
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
inputPlaceholder?: string;
|
||||
searchIcon: IconDefinition;
|
||||
onSearch: () => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default function SearchInputWithSelect(
|
||||
{
|
||||
selectValue,
|
||||
setSelectValue,
|
||||
selectOptions,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
inputPlaceholder,
|
||||
searchIcon,
|
||||
onSearch,
|
||||
onKeyDown
|
||||
}: SearchInputWithSelectProps) {
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<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"
|
||||
>
|
||||
{selectOptions.map((option: SelectBoxProps) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</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"
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
components/form/SelectBox.tsx
Normal file
43
components/form/SelectBox.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, {ChangeEvent} from "react";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
|
||||
export interface SelectBoxFormProps {
|
||||
onChangeCallBack: (event: ChangeEvent<HTMLSelectElement>) => void,
|
||||
data: SelectBoxProps[],
|
||||
defaultValue: string | null | undefined,
|
||||
placeholder?: string,
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function SelectBox(
|
||||
{
|
||||
onChangeCallBack,
|
||||
data,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
disabled
|
||||
}: SelectBoxFormProps) {
|
||||
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' : ''}`}
|
||||
>
|
||||
{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}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
55
components/form/SubmitButtonWLoading.tsx
Normal file
55
components/form/SubmitButtonWLoading.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
79
components/form/SuggestFieldInput.tsx
Normal file
79
components/form/SuggestFieldInput.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
import React, {ChangeEvent, Dispatch, SetStateAction} from "react";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
|
||||
interface SuggestFieldInputProps {
|
||||
inputFieldName: string;
|
||||
inputFieldIcon?: any;
|
||||
searchTags: string;
|
||||
tagued: string[];
|
||||
handleTagSearch: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
handleAddTag: (characterId: string) => void;
|
||||
handleRemoveTag: (characterId: string) => void;
|
||||
filteredTags: () => SelectBoxProps[];
|
||||
showTagSuggestions: boolean;
|
||||
setShowTagSuggestions: Dispatch<SetStateAction<boolean>>;
|
||||
getTagLabel: (id: string) => string;
|
||||
}
|
||||
|
||||
export default function SuggestFieldInput(
|
||||
{
|
||||
inputFieldName,
|
||||
inputFieldIcon,
|
||||
searchTags,
|
||||
tagued,
|
||||
handleTagSearch,
|
||||
handleAddTag,
|
||||
handleRemoveTag,
|
||||
filteredTags,
|
||||
showTagSuggestions,
|
||||
setShowTagSuggestions,
|
||||
getTagLabel
|
||||
}: SuggestFieldInputProps) {
|
||||
return (
|
||||
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
|
||||
<InputField fieldName={inputFieldName} icon={inputFieldIcon} input={
|
||||
<div className="w-full mb-3 relative">
|
||||
<TextInput value={searchTags} setValue={handleTagSearch}
|
||||
onFocus={() => setShowTagSuggestions(searchTags.trim().length > 0)}
|
||||
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">
|
||||
{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"
|
||||
onClick={() => handleAddTag(character.value)}
|
||||
>
|
||||
{character.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tagued.length === 0 ? (
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
components/form/TextInput.tsx
Normal file
39
components/form/TextInput.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, {ChangeEvent} from "react";
|
||||
|
||||
interface TextInputProps {
|
||||
value: string;
|
||||
setValue?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
onFocus?: () => void;
|
||||
}
|
||||
|
||||
export default function TextInput(
|
||||
{
|
||||
value,
|
||||
setValue,
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
disabled = false,
|
||||
onFocus
|
||||
}: TextInputProps) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
readOnly={readOnly}
|
||||
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' : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
117
components/form/TexteAreaInput.tsx
Normal file
117
components/form/TexteAreaInput.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, {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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user