Add foundational components and logic for migration, UI design, and input handling
- Introduced foundational UI components (`Badge`, `LockCard`, `SectionHeader`, `AvatarIcon`, etc.) for flexible layouts and consistent design. - Added migration support with the `MigrationModal` component and backend integration for exporting/importing data between Electron and Tauri. - Extended form components with `TextAreaInput`, `OrderInput`, `ToggleField`, and `ToolbarSelect` for improved input handling. - Updated `ScribeShell` with migration popup logic to prompt users for data migration. - Integrated `AlertStack` for better alert handling and notification management. - Enhanced Rust/Tauri services with migration command implementations. - Added translations and styles for new components.
This commit is contained in:
72
components/form/ImageDropZone.tsx
Normal file
72
components/form/ImageDropZone.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
import {DragEvent, ReactNode, useRef, useState} from "react";
|
||||
import {ImagePlus} from "lucide-react";
|
||||
|
||||
interface ImageDropZoneProps {
|
||||
onFileSelect: (file: File) => void;
|
||||
accept?: string;
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
const acceptedTypes: string[] = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
|
||||
function ImageDropZone({onFileSelect, accept = "image/png, image/jpeg, image/webp", label}: ImageDropZoneProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
|
||||
function handleDragOver(e: DragEvent<HTMLDivElement>): void {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent<HTMLDivElement>): void {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent<HTMLDivElement>): void {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const file: File | undefined = e.dataTransfer.files?.[0];
|
||||
if (file && acceptedTypes.includes(file.type)) {
|
||||
onFileSelect(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(): void {
|
||||
inputRef.current?.click();
|
||||
}
|
||||
|
||||
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const file: File | undefined = e.target.files?.[0];
|
||||
if (file) {
|
||||
onFileSelect(file);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
className={`flex flex-col items-center justify-center gap-2 border-2 border-dashed rounded-lg p-6 cursor-pointer transition-colors
|
||||
${isDragging
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-secondary bg-dark-background hover:bg-tertiary'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
onChange={handleInputChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<ImagePlus className="w-8 h-8 text-muted"/>
|
||||
{label && <span className="text-text-primary text-sm">{label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageDropZone;
|
||||
72
components/form/OrderInput.tsx
Normal file
72
components/form/OrderInput.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, {ChangeEvent} from "react";
|
||||
import {Minus, Plus} from "lucide-react";
|
||||
import IconButton from "@/components/ui/IconButton";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
|
||||
interface OrderInputProps {
|
||||
value: string;
|
||||
setValue: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
placeholder: string;
|
||||
order: number;
|
||||
setOrder: (order: number) => void;
|
||||
onAdd: () => Promise<void>;
|
||||
isAddDisabled?: boolean;
|
||||
}
|
||||
|
||||
export default function OrderInput(
|
||||
{
|
||||
value,
|
||||
setValue,
|
||||
placeholder,
|
||||
order,
|
||||
setOrder,
|
||||
onAdd,
|
||||
isAddDisabled = false
|
||||
}: OrderInputProps) {
|
||||
|
||||
function decrementOrder(): void {
|
||||
if (order > 0) {
|
||||
setOrder(order - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function incrementOrder(): void {
|
||||
setOrder(order + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<IconButton
|
||||
icon={Minus}
|
||||
variant="muted"
|
||||
size="sm"
|
||||
onClick={decrementOrder}
|
||||
disabled={order <= 0}
|
||||
/>
|
||||
<span className="text-muted text-xs w-5 h-5 rounded-md bg-tertiary text-center leading-5">
|
||||
{order}
|
||||
</span>
|
||||
<IconButton
|
||||
icon={Plus}
|
||||
variant="muted"
|
||||
size="sm"
|
||||
onClick={incrementOrder}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
icon={Plus}
|
||||
variant="primary"
|
||||
onClick={onAdd}
|
||||
disabled={isAddDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
components/form/TextAreaInput.tsx
Normal file
84
components/form/TextAreaInput.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, {ChangeEvent, useEffect, useRef} 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 progressRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (progressRef.current && maxLength) {
|
||||
progressRef.current.style.width = `${getProgressPercentage()}%`;
|
||||
}
|
||||
});
|
||||
|
||||
function getProgressPercentage(): number {
|
||||
if (!maxLength) return 0;
|
||||
return Math.min((value.length / maxLength) * 100, 100);
|
||||
}
|
||||
|
||||
function getStatusStyles(): { textColor: string; progressColor: string } | Record<string, never> {
|
||||
if (!maxLength) return {};
|
||||
const percentage = getProgressPercentage();
|
||||
|
||||
if (percentage >= 100) return {
|
||||
textColor: 'text-error',
|
||||
progressColor: 'bg-error'
|
||||
};
|
||||
|
||||
if (percentage >= 75) return {
|
||||
textColor: 'text-warning',
|
||||
progressColor: 'bg-warning'
|
||||
};
|
||||
|
||||
return {
|
||||
textColor: 'text-muted',
|
||||
progressColor: 'bg-primary'
|
||||
};
|
||||
}
|
||||
|
||||
const styles = getStatusStyles();
|
||||
|
||||
return (
|
||||
<div className="flex-grow flex-col flex h-full">
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
placeholder={placeholder}
|
||||
rows={3}
|
||||
className={`input-base w-full flex-grow p-3 lg:p-4 resize-none ${
|
||||
maxLength && value.length >= maxLength
|
||||
? 'border-error'
|
||||
: ''
|
||||
} h-full min-h-[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">
|
||||
<div className="w-24 h-1.5 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
ref={progressRef}
|
||||
className={`h-full rounded-full transition-all duration-500 ease-out ${styles.progressColor}`}
|
||||
></div>
|
||||
</div>
|
||||
<span className={`text-xs font-medium ${styles.textColor}`}>
|
||||
{value.length}/{maxLength}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
components/form/ToggleField.tsx
Normal file
49
components/form/ToggleField.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import {LucideIcon} from "lucide-react";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import ToggleWithConfirmation from "@/components/form/ToggleWithConfirmation";
|
||||
import {AlertType} from "@/components/ui/AlertBox";
|
||||
|
||||
interface ToggleFieldProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
alertTitle: string;
|
||||
alertMessage: string;
|
||||
alertType: AlertType;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export default function ToggleField(
|
||||
{
|
||||
icon,
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
alertTitle,
|
||||
alertMessage,
|
||||
alertType,
|
||||
confirmText,
|
||||
cancelText,
|
||||
}: ToggleFieldProps) {
|
||||
return (
|
||||
<InputField
|
||||
icon={icon}
|
||||
fieldName={label}
|
||||
centered
|
||||
input={
|
||||
<ToggleWithConfirmation
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
alertTitle={alertTitle}
|
||||
alertMessage={alertMessage}
|
||||
alertType={alertType}
|
||||
confirmText={confirmText}
|
||||
cancelText={cancelText}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
43
components/form/ToolbarSelect.tsx
Normal file
43
components/form/ToolbarSelect.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, {ChangeEvent} from "react";
|
||||
import {SelectBoxProps} from "@/components/form/SelectBox";
|
||||
|
||||
interface ToolbarSelectProps {
|
||||
onChangeCallBack: (event: ChangeEvent<HTMLSelectElement>) => void;
|
||||
data: SelectBoxProps[];
|
||||
defaultValue: string | null | undefined;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ToolbarSelect(
|
||||
{
|
||||
onChangeCallBack,
|
||||
data,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
disabled
|
||||
}: ToolbarSelectProps): React.JSX.Element {
|
||||
return (
|
||||
<select
|
||||
onChange={onChangeCallBack}
|
||||
disabled={disabled}
|
||||
key={defaultValue || 'placeholder'}
|
||||
defaultValue={defaultValue || '0'}
|
||||
className="bg-transparent text-text-primary text-xs px-2 py-1.5 rounded-lg
|
||||
border border-transparent
|
||||
hover:bg-secondary hover:border-secondary
|
||||
focus:bg-secondary focus:border-primary focus:outline-none
|
||||
transition-all duration-200 cursor-pointer
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{placeholder && <option value={'0'}>{placeholder}</option>}
|
||||
{data.map(function (item: SelectBoxProps): React.JSX.Element {
|
||||
return (
|
||||
<option key={item.value} value={item.value} className="bg-tertiary text-text-primary">
|
||||
{item.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user