readysite / website / frontend / pages / Partials.jsx
12.2 KB
Partials.jsx
import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { apiGet, apiPost, apiPatch, apiDelete } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';

export function Partials() {
  const { toggleChat } = useChat() || {};
  const [partials, setPartials] = useState([]);
  const [loading, setLoading] = useState(true);
  const [showModal, setShowModal] = useState(false);
  const [editPartial, setEditPartial] = useState(null);
  const [saving, setSaving] = useState(false);

  const fetchPartials = useCallback(() => {
    setLoading(true);
    apiGet('/api/admin/partials')
      .then((data) => setPartials(data?.items || data || []))
      .finally(() => setLoading(false));
  }, []);

  useEffect(() => {
    fetchPartials();
  }, [fetchPartials]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const form = e.target;
    const formData = new FormData(form);

    setSaving(true);
    try {
      if (editPartial) {
        await apiPatch(`/api/admin/partials/${editPartial.id}`, {
          name: formData.get('name'),
          description: formData.get('description'),
          html: formData.get('html'),
          published: formData.get('published') === 'on',
        });
      } else {
        await apiPost('/api/admin/partials', {
          name: formData.get('name'),
          slug: formData.get('slug') || undefined,
          description: formData.get('description'),
          html: formData.get('html'),
          published: formData.get('published') === 'on',
        });
      }
      setShowModal(false);
      setEditPartial(null);
      form.reset();
      fetchPartials();
    } finally {
      setSaving(false);
    }
  };

  const openEdit = (partial) => {
    setEditPartial(partial);
    setShowModal(true);
  };

  const openNew = () => {
    setEditPartial(null);
    setShowModal(true);
  };

  const deletePartial = async (id) => {
    if (!confirm('Delete this partial?')) return;
    await apiDelete(`/api/admin/partials/${id}`);
    fetchPartials();
  };

  return (
    <div className="flex flex-col h-full">
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-3 border-b border-base-300 bg-base-200/50">
        <div>
          <h2 className="font-semibold">Partials</h2>
          <p className="text-xs text-base-content/50">Reusable components</p>
        </div>
        <div className="flex items-center gap-2">
          <Link to="/pages" className="btn btn-ghost btn-sm gap-1">
            <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
            </svg>
            Pages
          </Link>
          <button onClick={openNew} className="btn btn-ghost btn-sm gap-1">
            <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
            </svg>
            Partial
          </button>
          {toggleChat && (
            <button onClick={toggleChat} className="btn btn-soft btn-primary btn-sm gap-2">
              <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
              </svg>
              AI Assistant
            </button>
          )}
        </div>
      </div>

      {/* Content */}
      <div className="flex-1 overflow-auto">
        {loading ? (
          <div className="flex justify-center py-12">
            <span className="loading loading-spinner loading-lg" />
          </div>
        ) : partials.length ? (
          <table className="table table-pin-rows table-sm">
            <thead>
              <tr className="bg-base-200">
                <th className="font-medium text-base-content/70">Name</th>
                <th className="font-medium text-base-content/70">Description</th>
                <th className="font-medium text-base-content/70">Usage</th>
                <th className="font-medium text-base-content/70">Status</th>
                <th className="w-24"></th>
              </tr>
            </thead>
            <tbody>
              {partials.map((p) => (
                <tr key={p.id} className="hover:bg-base-200/50 cursor-pointer" onClick={() => openEdit(p)}>
                  <td className="font-medium">{p.name}</td>
                  <td className="max-w-xs truncate text-base-content/70">{p.description}</td>
                  <td className="font-mono text-xs text-base-content/50">{`{{partial "${p.id}"}}`}</td>
                  <td>
                    {p.published ? (
                      <span className="badge badge-success badge-xs">Published</span>
                    ) : (
                      <span className="badge badge-ghost badge-xs">Draft</span>
                    )}
                  </td>
                  <td>
                    <div className="flex gap-1 justify-end">
                      <button
                        onClick={(e) => { e.stopPropagation(); openEdit(p); }}
                        className="btn btn-ghost btn-xs"
                      >
                        Edit
                      </button>
                      <button
                        onClick={(e) => { e.stopPropagation(); deletePartial(p.id); }}
                        className="btn btn-ghost btn-xs text-error"
                      >
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                        </svg>
                      </button>
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        ) : (
          <div className="flex flex-col items-center justify-center h-full text-center px-6">
            <div className="w-16 h-16 rounded-2xl bg-base-200 flex items-center justify-center mb-4">
              <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
              </svg>
            </div>
            <h3 className="text-lg font-semibold mb-1">No partials yet</h3>
            <p className="text-base-content/50 text-sm mb-4">Create reusable HTML components for your pages.</p>
            <button onClick={openNew} className="btn btn-primary btn-sm">Create First Partial</button>
          </div>
        )}
      </div>

      {/* New/Edit Partial Modal */}
      {showModal && (
        <dialog className="modal modal-open">
          <div className="modal-box w-11/12 max-w-3xl max-h-[90vh]">
            <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => { setShowModal(false); setEditPartial(null); }}>✕</button>
            <h3 className="font-bold text-lg mb-4">{editPartial ? 'Edit Partial' : 'New Partial'}</h3>

            <form onSubmit={handleSubmit} className="space-y-4">
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                <div className="form-control">
                  <label className="floating-label">
                    <span>Name <span className="text-error">*</span></span>
                    <input type="text" name="name" placeholder="Header" className="input input-bordered w-full" required autoFocus maxLength="100" defaultValue={editPartial?.name || ''} />
                  </label>
                </div>

                {!editPartial && (
                  <div className="form-control">
                    <label className="floating-label">
                      <span>Slug (ID)</span>
                      <input type="text" name="slug" placeholder="header" className="input input-bordered w-full" maxLength="50" pattern="[a-z0-9-]+" />
                    </label>
                    <label className="label">
                      <span className="label-text-alt text-base-content/50">URL-friendly ID (auto-generated if empty)</span>
                    </label>
                  </div>
                )}

                {editPartial && (
                  <div className="form-control">
                    <label className="floating-label">
                      <span>Slug (ID)</span>
                      <input type="text" className="input input-bordered w-full bg-base-200" value={editPartial.id} readOnly />
                    </label>
                    <label className="label">
                      <span className="label-text-alt text-base-content/50">ID cannot be changed</span>
                    </label>
                  </div>
                )}
              </div>

              <div className="form-control">
                <label className="floating-label">
                  <span>Description</span>
                  <input type="text" name="description" placeholder="Site header with navigation" className="input input-bordered w-full" maxLength="200" defaultValue={editPartial?.description || ''} />
                </label>
                <label className="label">
                  <span className="label-text-alt text-base-content/50">Helps AI understand what this partial is for</span>
                </label>
              </div>

              <div className="form-control">
                <label className="label">
                  <span className="label-text font-medium">HTML Content <span className="text-error">*</span></span>
                </label>
                <textarea
                  name="html"
                  placeholder="<header>...</header>"
                  className="textarea textarea-bordered w-full font-mono text-sm bg-base-100"
                  rows="12"
                  required
                  defaultValue={editPartial?.html || ''}
                />
              </div>

              <div className="form-control">
                <label className="label cursor-pointer justify-start gap-4">
                  <input type="checkbox" name="published" className="checkbox checkbox-primary checkbox-sm" defaultChecked={editPartial?.published ?? true} />
                  <span className="label-text">Published (available for use in pages)</span>
                </label>
              </div>

              <div className="flex justify-end gap-2 pt-4">
                {editPartial && (
                  <button
                    type="button"
                    className="btn btn-ghost btn-sm text-error mr-auto"
                    onClick={() => { deletePartial(editPartial.id); setShowModal(false); setEditPartial(null); }}
                  >
                    Delete
                  </button>
                )}
                <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setShowModal(false); setEditPartial(null); }}>Cancel</button>
                <button type="submit" className="btn btn-primary btn-sm" disabled={saving}>
                  {saving ? <span className="loading loading-spinner loading-sm" /> : (editPartial ? 'Save Changes' : 'Create Partial')}
                </button>
              </div>
            </form>
          </div>
          <div className="modal-backdrop" onClick={() => { setShowModal(false); setEditPartial(null); }} />
        </dialog>
      )}
    </div>
  );
}
← Back