| [e9a0543] | 1 | import React, { useEffect, useMemo, useState } from "react";
|
|---|
| 2 | import { createPortal } from "react-dom";
|
|---|
| 3 |
|
|---|
| 4 | export default function AddCustomCategoryModal({ open, onClose, onSubmit }) {
|
|---|
| 5 | const [name, setName] = useState("");
|
|---|
| 6 | const [submitting, setSubmitting] = useState(false);
|
|---|
| 7 | const [error, setError] = useState(null);
|
|---|
| 8 |
|
|---|
| 9 | // Reset the form whenever the modal is opened.
|
|---|
| 10 | // NOTE: Hooks must be called in the same order on every render; do not early-return before hooks.
|
|---|
| 11 | useEffect(() => {
|
|---|
| 12 | if (!open) return;
|
|---|
| 13 | setName("");
|
|---|
| 14 | setError(null);
|
|---|
| 15 | setSubmitting(false);
|
|---|
| 16 | }, [open]);
|
|---|
| 17 |
|
|---|
| 18 | const canSubmit = useMemo(() => {
|
|---|
| 19 | return name.trim().length > 0 && name.trim().length <= 100 && !submitting;
|
|---|
| 20 | }, [name, submitting]);
|
|---|
| 21 |
|
|---|
| 22 | async function handleSubmit(e) {
|
|---|
| 23 | e.preventDefault();
|
|---|
| 24 | if (!canSubmit) return;
|
|---|
| 25 |
|
|---|
| 26 | setSubmitting(true);
|
|---|
| 27 | setError(null);
|
|---|
| 28 | try {
|
|---|
| 29 | await onSubmit({ name: name.trim() });
|
|---|
| 30 | onClose?.();
|
|---|
| 31 | } catch (err) {
|
|---|
| 32 | setError(
|
|---|
| 33 | err?.response?.data?.message ||
|
|---|
| 34 | err?.message ||
|
|---|
| 35 | "Failed to create custom category"
|
|---|
| 36 | );
|
|---|
| 37 | setSubmitting(false);
|
|---|
| 38 | }
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | if (!open) return null;
|
|---|
| 42 |
|
|---|
| 43 | return createPortal(
|
|---|
| 44 | <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|---|
| 45 | <button
|
|---|
| 46 | type="button"
|
|---|
| 47 | className="absolute inset-0 bg-black/60"
|
|---|
| 48 | aria-label="Close modal"
|
|---|
| 49 | onClick={onClose}
|
|---|
| 50 | />
|
|---|
| 51 |
|
|---|
| 52 | <div className="relative w-full max-w-md rounded-xl bg-neutral-900 p-5 text-white shadow-xl">
|
|---|
| 53 | <div className="flex items-center justify-between gap-4">
|
|---|
| 54 | <h2 className="text-lg font-semibold">Add Custom Category</h2>
|
|---|
| 55 | <button
|
|---|
| 56 | type="button"
|
|---|
| 57 | onClick={onClose}
|
|---|
| 58 | className="rounded-md px-2 py-1 text-sm text-white/80 hover:bg-white/10"
|
|---|
| 59 | >
|
|---|
| 60 | Close
|
|---|
| 61 | </button>
|
|---|
| 62 | </div>
|
|---|
| 63 |
|
|---|
| 64 | <form onSubmit={handleSubmit} className="mt-4 space-y-3">
|
|---|
| 65 | <div>
|
|---|
| 66 | <label className="mb-1 block text-sm text-white/80">Name</label>
|
|---|
| 67 | <input
|
|---|
| 68 | value={name}
|
|---|
| 69 | onChange={(e) => setName(e.target.value)}
|
|---|
| 70 | maxLength={100}
|
|---|
| 71 | autoFocus
|
|---|
| 72 | className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-sm outline-none focus:border-green-400"
|
|---|
| 73 | placeholder="e.g. My Habit Tracking"
|
|---|
| 74 | />
|
|---|
| 75 | <div className="mt-1 text-xs text-white/50">
|
|---|
| 76 | {name.trim().length}/100
|
|---|
| 77 | </div>
|
|---|
| 78 | </div>
|
|---|
| 79 |
|
|---|
| 80 | {error ? (
|
|---|
| 81 | <div className="rounded-lg border border-red-500/30 bg-red-500/10 p-2 text-sm text-red-200">
|
|---|
| 82 | {error}
|
|---|
| 83 | </div>
|
|---|
| 84 | ) : null}
|
|---|
| 85 |
|
|---|
| 86 | <button
|
|---|
| 87 | type="submit"
|
|---|
| 88 | disabled={!canSubmit}
|
|---|
| 89 | className="w-full rounded-lg bg-green-400 px-3 py-2 text-sm font-medium text-black disabled:cursor-not-allowed disabled:opacity-60"
|
|---|
| 90 | >
|
|---|
| 91 | {submitting ? "Creating..." : "Create"}
|
|---|
| 92 | </button>
|
|---|
| 93 | </form>
|
|---|
| 94 | </div>
|
|---|
| 95 | </div>,
|
|---|
| 96 | document.body
|
|---|
| 97 | );
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 |
|
|---|
| 101 |
|
|---|