154 lines
4.0 KiB
TypeScript
154 lines
4.0 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
IonButton,
|
|
IonButtons,
|
|
IonContent,
|
|
IonHeader,
|
|
IonInput,
|
|
IonItem,
|
|
IonLabel,
|
|
IonModal,
|
|
IonTextarea,
|
|
IonTitle,
|
|
IonToolbar,
|
|
} from '@ionic/react';
|
|
|
|
interface HabitFormModalProps {
|
|
isOpen: boolean;
|
|
onDismiss: () => void;
|
|
onSubmit: (values: {
|
|
name: string;
|
|
targetDescription: string;
|
|
color: string;
|
|
}) => Promise<void>;
|
|
initialValues?: {
|
|
name: string;
|
|
targetDescription: string;
|
|
color: string;
|
|
};
|
|
}
|
|
|
|
const COLORS = [
|
|
'#7c3aed',
|
|
'#0f766e',
|
|
'#ea580c',
|
|
'#dc2626',
|
|
'#2563eb',
|
|
'#059669',
|
|
];
|
|
|
|
const HabitFormModal: React.FC<HabitFormModalProps> = ({
|
|
isOpen,
|
|
onDismiss,
|
|
onSubmit,
|
|
initialValues,
|
|
}) => {
|
|
const isEditing = !!initialValues;
|
|
const [name, setName] = useState(initialValues?.name ?? '');
|
|
const [targetDescription, setTargetDescription] = useState(
|
|
initialValues?.targetDescription ?? ''
|
|
);
|
|
const [color, setColor] = useState(initialValues?.color ?? COLORS[0]);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const canSubmit = useMemo(
|
|
() => name.trim().length > 0 && name.trim().length <= 80,
|
|
[name]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setName(initialValues?.name ?? '');
|
|
setTargetDescription(initialValues?.targetDescription ?? '');
|
|
setColor(initialValues?.color ?? COLORS[0]);
|
|
setError('');
|
|
}
|
|
}, [isOpen, initialValues]);
|
|
|
|
const handleSubmit = async () => {
|
|
setError('');
|
|
setSaving(true);
|
|
try {
|
|
await onSubmit({
|
|
name: name.trim(),
|
|
targetDescription: targetDescription.trim(),
|
|
color,
|
|
});
|
|
setName('');
|
|
setTargetDescription('');
|
|
setColor(COLORS[0]);
|
|
onDismiss();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unable to save habit.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<IonModal isOpen={isOpen} onDidDismiss={onDismiss}>
|
|
<IonHeader className="ion-no-border">
|
|
<IonToolbar>
|
|
<IonTitle>{isEditing ? 'Edit Habit' : 'New Habit'}</IonTitle>
|
|
<IonButtons slot="end">
|
|
<IonButton onClick={onDismiss}>Close</IonButton>
|
|
</IonButtons>
|
|
</IonToolbar>
|
|
</IonHeader>
|
|
<IonContent className="ion-padding">
|
|
<div className="habit-form-grid">
|
|
<IonItem lines="none" className="auth-input-item">
|
|
<IonInput
|
|
label="Habit name"
|
|
labelPlacement="stacked"
|
|
value={name}
|
|
maxlength={80}
|
|
placeholder="Drink water"
|
|
onIonInput={(e) => setName(e.detail.value ?? '')}
|
|
/>
|
|
</IonItem>
|
|
|
|
<IonItem lines="none" className="auth-input-item">
|
|
<IonTextarea
|
|
label="Target or note"
|
|
labelPlacement="stacked"
|
|
value={targetDescription}
|
|
placeholder="8 glasses, 20 minutes, 30 pushups..."
|
|
autoGrow
|
|
onIonInput={(e) => setTargetDescription(e.detail.value ?? '')}
|
|
/>
|
|
</IonItem>
|
|
|
|
<div>
|
|
<IonLabel className="color-label">Color</IonLabel>
|
|
<div className="habit-color-row">
|
|
{COLORS.map((swatch) => (
|
|
<button
|
|
key={swatch}
|
|
type="button"
|
|
className={`habit-color-dot ${color === swatch ? 'selected' : ''}`}
|
|
style={{ background: swatch }}
|
|
onClick={() => setColor(swatch)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{error ? <p className="auth-feedback error-text">{error}</p> : null}
|
|
|
|
<IonButton
|
|
expand="block"
|
|
onClick={handleSubmit}
|
|
disabled={!canSubmit || saving}
|
|
>
|
|
{saving ? 'Saving…' : isEditing ? 'Save Changes' : 'Create Habit'}
|
|
</IonButton>
|
|
</div>
|
|
</IonContent>
|
|
</IonModal>
|
|
);
|
|
};
|
|
|
|
export default HabitFormModal;
|