← back to Norma

components/drafts/VersionHistory.tsx

188 lines

'use client';

import { useState, useEffect } from 'react';
import { History, RotateCcw, Plus } from 'lucide-react';

interface DraftVersion {
  id: string;
  version_number: number;
  subject: string;
  body_html: string;
  body_text?: string;
  change_summary?: string;
  created_at: string;
  created_by?: string;
}

interface VersionHistoryProps {
  draftId: string;
  onRestore: (version: DraftVersion) => void;
}

function formatDate(iso: string) {
  return new Date(iso).toLocaleString('en-US', {
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    hour12: true,
    timeZoneName: 'short',
  });
}

export default function VersionHistory({ draftId, onRestore }: VersionHistoryProps) {
  const [versions, setVersions] = useState<DraftVersion[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);
  const [restoring, setRestoring] = useState<string | null>(null);

  async function fetchVersions() {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(`/api/drafts/${draftId}/versions`);
      if (!res.ok) throw new Error('Failed to load versions');
      const data = await res.json();
      setVersions(data.versions ?? data ?? []);
    } catch {
      setError('Failed to load version history.');
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => { fetchVersions(); }, [draftId]); // eslint-disable-line react-hooks/exhaustive-deps

  async function handleSaveVersion() {
    setSaving(true);
    try {
      const res = await fetch(`/api/drafts/${draftId}/versions`, { method: 'POST' });
      if (!res.ok) throw new Error('Save failed');
      await fetchVersions();
    } catch {
      alert('Failed to save version snapshot.');
    } finally {
      setSaving(false);
    }
  }

  async function handleRestore(version: DraftVersion) {
    if (!confirm(`Restore to version ${version.version_number}? Current unsaved changes will be lost.`)) return;
    setRestoring(version.id);
    onRestore(version);
    setRestoring(null);
  }

  return (
    <div className="flex flex-col h-full">
      {/* Header */}
      <div
        className="flex items-center justify-between px-4 py-3 shrink-0"
        style={{ borderBottom: '1px solid var(--color-border)' }}
      >
        <div className="flex items-center gap-2">
          <History size={15} style={{ color: 'var(--color-text-secondary)' }} aria-hidden="true" />
          <h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
            Version History
          </h3>
          {!loading && (
            <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
              ({versions.length})
            </span>
          )}
        </div>
        <button
          type="button"
          onClick={handleSaveVersion}
          disabled={saving}
          className="btn btn-secondary btn-sm"
          title="Save current state as a version snapshot"
        >
          <Plus size={12} aria-hidden="true" />
          {saving ? 'Saving...' : 'Save Version'}
        </button>
      </div>

      {/* List */}
      <div className="flex-1 overflow-y-auto">
        {loading && (
          <div className="flex justify-center py-6">
            <span className="spinner" aria-label="Loading versions" />
          </div>
        )}
        {error && (
          <p className="text-sm text-center px-4 py-6" style={{ color: 'var(--color-error)' }}>
            {error}
          </p>
        )}
        {!loading && !error && versions.length === 0 && (
          <div className="flex flex-col items-center justify-center gap-2 py-10">
            <History size={24} style={{ color: 'var(--color-text-muted)' }} />
            <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
              No saved versions yet.
            </p>
            <p className="text-xs text-center px-6" style={{ color: 'var(--color-text-muted)' }}>
              Click "Save Version" to create a manual snapshot.
            </p>
          </div>
        )}
        {!loading && !error && versions.length > 0 && (
          <ul className="divide-y" style={{ borderColor: 'var(--color-border)' }}>
            {versions.map((v, i) => (
              <li
                key={v.id}
                className="flex items-start justify-between gap-3 px-4 py-3"
                style={{
                  backgroundColor: i === 0 ? 'rgba(16, 185, 129, 0.04)' : 'transparent',
                }}
              >
                <div className="flex-1 min-w-0">
                  <div className="flex items-center gap-2 mb-0.5">
                    <span
                      className="text-xs font-semibold"
                      style={{ color: i === 0 ? 'var(--color-primary)' : 'var(--color-text)' }}
                    >
                      v{v.version_number}
                    </span>
                    {i === 0 && (
                      <span
                        className="text-xs px-1.5 py-0.5 rounded"
                        style={{
                          backgroundColor: 'rgba(16, 185, 129, 0.15)',
                          color: 'var(--color-primary)',
                          border: '1px solid rgba(16, 185, 129, 0.3)',
                        }}
                      >
                        Latest
                      </span>
                    )}
                  </div>
                  <p className="text-xs truncate mb-0.5" style={{ color: 'var(--color-text-secondary)' }}>
                    {v.change_summary ?? 'Manual snapshot'}
                  </p>
                  <p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                    {formatDate(v.created_at)}
                  </p>
                </div>
                <button
                  type="button"
                  onClick={() => handleRestore(v)}
                  disabled={restoring === v.id}
                  className="btn btn-ghost btn-sm shrink-0"
                  title={`Restore version ${v.version_number}`}
                >
                  <RotateCcw size={12} aria-hidden="true" />
                  <span className="hidden sm:inline">
                    {restoring === v.id ? 'Restoring...' : 'Restore'}
                  </span>
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}