← back to Grant

components/grants/AddGrantWizard.tsx

551 lines

'use client';

import { useState, useEffect, useCallback, useRef } from 'react';
import { X, Loader2, Plus, Check, ChevronRight, ChevronLeft } from 'lucide-react';
import { useToast } from '../ToastProvider';

interface AddGrantWizardProps {
  onClose: () => void;
  onCreated: () => void;
}

const STEPS = [
  { label: 'Basic Info', number: 1 },
  { label: 'Details', number: 2 },
  { label: 'Priority & Metadata', number: 3 },
];

export default function AddGrantWizard({ onClose, onCreated }: AddGrantWizardProps) {
  const { addToast } = useToast();
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);
  const [step, setStep] = useState(1);
  const [saving, setSaving] = useState(false);
  const [titleError, setTitleError] = useState(false);

  // Store previously focused element and focus the modal on mount
  useEffect(() => {
    previousFocusRef.current = document.activeElement as HTMLElement;
    modalRef.current?.focus();
    return () => {
      previousFocusRef.current?.focus();
    };
  }, []);

  // Step 1 fields
  const [title, setTitle] = useState('');
  const [funder, setFunder] = useState('');
  const [amountMin, setAmountMin] = useState('');
  const [amountMax, setAmountMax] = useState('');
  const [deadline, setDeadline] = useState('');

  // Step 2 fields
  const [description, setDescription] = useState('');
  const [eligibility, setEligibility] = useState('');
  const [focusAreas, setFocusAreas] = useState<string[]>([]);
  const [focusInput, setFocusInput] = useState('');
  const [applicationUrl, setApplicationUrl] = useState('');

  // Step 3 fields
  const [priority, setPriority] = useState('medium');
  const [cycle, setCycle] = useState('');
  const [tags, setTags] = useState('');
  const [notes, setNotes] = useState('');

  // Escape to close + focus trapping
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose();
      return;
    }
    if (e.key === 'Tab' && modalRef.current) {
      const focusable = modalRef.current.querySelectorAll<HTMLElement>(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      if (focusable.length === 0) return;
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (e.shiftKey) {
        if (document.activeElement === first) {
          e.preventDefault();
          last.focus();
        }
      } else {
        if (document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    }
  }, [onClose]);

  const handleNext = useCallback(() => {
    if (step === 1) {
      if (!title.trim()) {
        setTitleError(true);
        return;
      }
      setTitleError(false);
    }
    setStep((s) => Math.min(s + 1, 3));
  }, [step, title]);

  const handleBack = useCallback(() => {
    setStep((s) => Math.max(s - 1, 1));
  }, []);

  const handleFocusKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      const val = focusInput.trim();
      if (val && !focusAreas.includes(val)) {
        setFocusAreas((prev) => [...prev, val]);
      }
      setFocusInput('');
    }
  };

  const removeFocusArea = (area: string) => {
    setFocusAreas((prev) => prev.filter((a) => a !== area));
  };

  const handleSubmit = async () => {
    setSaving(true);
    try {
      const res = await fetch('/api/grants', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({
          title: title.trim(),
          funder: funder.trim() || undefined,
          amount_min: amountMin ? Number(amountMin) : null,
          amount_max: amountMax ? Number(amountMax) : null,
          deadline: deadline || null,
          description: description.trim() || null,
          eligibility: eligibility.trim() || null,
          focus_areas: focusAreas,
          application_url: applicationUrl.trim() || null,
          priority,
          cycle: cycle || null,
          tags: tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : [],
          notes: notes.trim() || null,
        }),
      });
      if (res.ok) {
        onCreated();
        onClose();
        addToast('Grant added successfully', 'success');
      } else {
        addToast('Failed to add grant', 'error');
      }
    } catch {
      addToast('Failed to add grant', 'error');
    } finally {
      setSaving(false);
    }
  };

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center p-4"
      style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
      onClick={onClose}
      role="dialog"
      aria-modal="true"
      aria-labelledby="add-grant-modal-title"
      onKeyDown={handleKeyDown}
    >
      <div
        ref={modalRef}
        className="card w-full max-w-lg max-h-[85vh] overflow-auto"
        style={{ backgroundColor: 'var(--color-surface)' }}
        onClick={(e) => e.stopPropagation()}
        tabIndex={-1}
      >
        {/* Header */}
        <div className="flex items-center justify-between mb-5">
          <h3 id="add-grant-modal-title" className="text-base font-semibold" style={{ color: 'var(--color-text)' }}>
            Add Grant
          </h3>
          <button className="btn btn-ghost btn-sm" onClick={onClose} aria-label="Close dialog">
            <X size={16} />
          </button>
        </div>

        {/* Step Indicator */}
        <div className="flex items-center justify-center mb-6">
          {STEPS.map((s, i) => (
            <div key={s.number} className="flex items-center">
              {/* Circle */}
              <div className="flex flex-col items-center">
                <div
                  style={{
                    width: 32,
                    height: 32,
                    borderRadius: '50%',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    fontSize: '0.75rem',
                    fontWeight: 700,
                    transition: 'all 150ms ease',
                    backgroundColor:
                      step > s.number
                        ? '#059669'
                        : step === s.number
                          ? '#059669'
                          : 'var(--color-surface-el)',
                    color:
                      step >= s.number ? '#fff' : 'var(--color-text-muted)',
                    border:
                      step >= s.number
                        ? '2px solid #059669'
                        : '2px solid var(--color-border)',
                  }}
                >
                  {step > s.number ? <Check size={14} /> : s.number}
                </div>
                <span
                  className="text-xs mt-1"
                  style={{
                    color:
                      step === s.number
                        ? 'var(--color-text)'
                        : 'var(--color-text-muted)',
                    fontWeight: step === s.number ? 600 : 400,
                  }}
                >
                  {s.label}
                </span>
              </div>
              {/* Connector line */}
              {i < STEPS.length - 1 && (
                <div
                  style={{
                    width: 48,
                    height: 2,
                    backgroundColor:
                      step > s.number ? '#059669' : 'var(--color-border)',
                    marginLeft: 8,
                    marginRight: 8,
                    marginBottom: 18,
                    transition: 'background-color 150ms ease',
                  }}
                />
              )}
            </div>
          ))}
        </div>

        {/* Step 1 — Basic Info */}
        {step === 1 && (
          <div className="space-y-3">
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Title *
              </label>
              <input
                className="input"
                placeholder="Grant program name"
                value={title}
                onChange={(e) => {
                  setTitle(e.target.value);
                  if (e.target.value.trim()) setTitleError(false);
                }}
                style={
                  titleError
                    ? { borderColor: '#ef4444', boxShadow: '0 0 0 3px rgba(239,68,68,0.2)' }
                    : undefined
                }
              />
              {titleError && (
                <p className="text-xs mt-1" style={{ color: '#ef4444' }}>
                  Title is required
                </p>
              )}
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Funder
              </label>
              <input
                className="input"
                placeholder="Foundation or agency name"
                value={funder}
                onChange={(e) => setFunder(e.target.value)}
              />
            </div>
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
              <div>
                <label
                  className="block text-xs font-medium mb-1"
                  style={{ color: 'var(--color-text-secondary)' }}
                >
                  Min Amount ($)
                </label>
                <input
                  className="input"
                  type="number"
                  placeholder="10000"
                  value={amountMin}
                  onChange={(e) => setAmountMin(e.target.value)}
                />
              </div>
              <div>
                <label
                  className="block text-xs font-medium mb-1"
                  style={{ color: 'var(--color-text-secondary)' }}
                >
                  Max Amount ($)
                </label>
                <input
                  className="input"
                  type="number"
                  placeholder="50000"
                  value={amountMax}
                  onChange={(e) => setAmountMax(e.target.value)}
                />
              </div>
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Deadline
              </label>
              <input
                className="input"
                type="date"
                value={deadline}
                onChange={(e) => setDeadline(e.target.value)}
              />
            </div>
          </div>
        )}

        {/* Step 2 — Details */}
        {step === 2 && (
          <div className="space-y-3">
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Description
              </label>
              <textarea
                className="input"
                rows={4}
                placeholder="What does this grant fund?"
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                style={{ resize: 'vertical' }}
              />
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Eligibility
              </label>
              <textarea
                className="input"
                rows={3}
                placeholder="Who is eligible to apply?"
                value={eligibility}
                onChange={(e) => setEligibility(e.target.value)}
                style={{ resize: 'vertical' }}
              />
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Focus Areas
              </label>
              <input
                className="input"
                placeholder="Type a focus area and press Enter"
                value={focusInput}
                onChange={(e) => setFocusInput(e.target.value)}
                onKeyDown={handleFocusKeyDown}
              />
              {focusAreas.length > 0 && (
                <div className="flex flex-wrap gap-1.5 mt-2">
                  {focusAreas.map((area) => (
                    <span
                      key={area}
                      className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full"
                      style={{
                        backgroundColor: 'rgba(5,150,105,0.12)',
                        color: '#34d399',
                        border: '1px solid rgba(5,150,105,0.25)',
                      }}
                    >
                      {area}
                      <button
                        type="button"
                        onClick={() => removeFocusArea(area)}
                        aria-label={`Remove ${area}`}
                        style={{
                          background: 'none',
                          border: 'none',
                          color: '#34d399',
                          cursor: 'pointer',
                          padding: 0,
                          lineHeight: 1,
                          fontSize: '0.875rem',
                        }}
                      >
                        <X size={12} />
                      </button>
                    </span>
                  ))}
                </div>
              )}
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Application URL
              </label>
              <input
                className="input"
                type="url"
                placeholder="https://..."
                value={applicationUrl}
                onChange={(e) => setApplicationUrl(e.target.value)}
              />
            </div>
          </div>
        )}

        {/* Step 3 — Priority & Metadata */}
        {step === 3 && (
          <div className="space-y-3">
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
              <div>
                <label
                  className="block text-xs font-medium mb-1"
                  style={{ color: 'var(--color-text-secondary)' }}
                >
                  Priority
                </label>
                <select
                  className="input"
                  value={priority}
                  onChange={(e) => setPriority(e.target.value)}
                >
                  <option value="high">High</option>
                  <option value="medium">Medium</option>
                  <option value="low">Low</option>
                </select>
              </div>
              <div>
                <label
                  className="block text-xs font-medium mb-1"
                  style={{ color: 'var(--color-text-secondary)' }}
                >
                  Cycle
                </label>
                <select
                  className="input"
                  value={cycle}
                  onChange={(e) => setCycle(e.target.value)}
                >
                  <option value="">Select cycle...</option>
                  <option value="Annual">Annual</option>
                  <option value="Biannual">Biannual</option>
                  <option value="Rolling">Rolling</option>
                  <option value="One-time">One-time</option>
                </select>
              </div>
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Tags (comma-separated)
              </label>
              <input
                className="input"
                placeholder="education, advocacy, youth"
                value={tags}
                onChange={(e) => setTags(e.target.value)}
              />
            </div>
            <div>
              <label
                className="block text-xs font-medium mb-1"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Notes
              </label>
              <textarea
                className="input"
                rows={3}
                placeholder="Any internal notes about this grant..."
                value={notes}
                onChange={(e) => setNotes(e.target.value)}
                style={{ resize: 'vertical' }}
              />
            </div>
          </div>
        )}

        {/* Navigation buttons */}
        <div className="flex gap-2 pt-5 mt-2" style={{ borderTop: '1px solid var(--color-border)' }}>
          {step > 1 && (
            <button
              type="button"
              className="btn btn-secondary"
              onClick={handleBack}
            >
              <ChevronLeft size={15} />
              Back
            </button>
          )}
          <div className="flex-1" />
          {step < 3 ? (
            <button
              type="button"
              className="btn btn-primary"
              onClick={handleNext}
            >
              Next
              <ChevronRight size={15} />
            </button>
          ) : (
            <button
              type="button"
              className="btn btn-primary"
              onClick={handleSubmit}
              disabled={saving}
            >
              {saving ? (
                <Loader2 size={15} className="animate-spin" />
              ) : (
                <Plus size={15} />
              )}
              {saving ? 'Creating...' : 'Create Grant'}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}