readysite / website / frontend / pages / Settings.jsx
11.9 KB
Settings.jsx
import * as React from 'react';
import { useState, useEffect } from 'react';
import { apiGet, apiPatch } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';

export function Settings() {
  const { toggleChat } = useChat() || {};
  const [settings, setSettings] = useState(null);
  const [loading, setLoading] = useState(true);
  const [savingSite, setSavingSite] = useState(false);
  const [savingAI, setSavingAI] = useState(false);
  const [provider, setProvider] = useState('anthropic');
  const [siteSuccess, setSiteSuccess] = useState(false);
  const [aiSuccess, setAISuccess] = useState(false);

  useEffect(() => {
    apiGet('/api/admin/settings')
      .then((data) => {
        setSettings(data);
        setProvider(data?.aiProvider || 'anthropic');
      })
      .finally(() => setLoading(false));
  }, []);

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

    setSavingSite(true);
    setSiteSuccess(false);
    try {
      const updated = await apiPatch('/api/admin/settings', {
        siteName: formData.get('site_name'),
        siteDescription: formData.get('site_description'),
      });
      setSettings((prev) => ({ ...prev, ...updated }));
      setSiteSuccess(true);
      setTimeout(() => setSiteSuccess(false), 2000);
    } finally {
      setSavingSite(false);
    }
  };

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

    setSavingAI(true);
    setAISuccess(false);
    try {
      const updated = await apiPatch('/api/admin/settings', {
        aiProvider: formData.get('ai_provider'),
        aiModel: formData.get('ai_model'),
        aiApiKey: formData.get('ai_api_key'),
      });
      setSettings((prev) => ({ ...prev, ...updated }));
      setAISuccess(true);
      setTimeout(() => setAISuccess(false), 2000);
      form.querySelector('input[name="ai_api_key"]').value = '';
    } finally {
      setSavingAI(false);
    }
  };

  const handleProviderChange = (e) => {
    setProvider(e.target.value);
  };

  const getModelOptions = () => {
    if (provider === 'openai') {
      return [
        { value: 'gpt-4o', label: 'GPT-4o (Recommended)' },
        { value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
        { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' },
      ];
    }
    return [
      { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5 (Recommended)' },
      { value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
    ];
  };

  // AI is configured if provider is set (not mock) and has an API key
  const aiConfigured = settings?.aiProvider && settings?.aiProvider !== 'mock' && settings?.hasApiKey;

  if (loading) {
    return (
      <div className="flex flex-col h-full">
        <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">Settings</h2>
            <p className="text-xs text-base-content/50">Site configuration</p>
          </div>
        </div>
        <div className="flex-1 flex justify-center items-center">
          <span className="loading loading-spinner loading-lg" />
        </div>
      </div>
    );
  }

  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">Settings</h2>
          <p className="text-xs text-base-content/50">Site configuration</p>
        </div>
        <div className="flex items-center gap-2">
          {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 p-6">
        <div className="max-w-2xl mx-auto space-y-6">
          {/* Site Settings */}
          <form onSubmit={handleSiteSubmit} className="card bg-base-200/50 border border-base-300">
            <div className="card-body">
              <h3 className="card-title text-lg">Site</h3>
              <p className="text-sm text-base-content/50 -mt-2 mb-4">Basic information about your website</p>

              <div className="space-y-4">
                <div className="form-control">
                  <label className="floating-label">
                    <span>Site Name</span>
                    <input
                      type="text"
                      name="site_name"
                      placeholder="My Website"
                      className="input input-bordered w-full"
                      defaultValue={settings?.siteName || ''}
                    />
                  </label>
                  <label className="label">
                    <span className="label-text-alt text-base-content/50">Displayed in the browser tab and search results</span>
                  </label>
                </div>

                <div className="form-control">
                  <label className="floating-label">
                    <span>Site Description</span>
                    <input
                      type="text"
                      name="site_description"
                      placeholder="A brief description of your website"
                      className="input input-bordered w-full"
                      defaultValue={settings?.siteDescription || ''}
                    />
                  </label>
                  <label className="label">
                    <span className="label-text-alt text-base-content/50">Used for SEO and social sharing</span>
                  </label>
                </div>
              </div>

              <div className="card-actions justify-end mt-4">
                {siteSuccess && (
                  <span className="text-success text-sm flex items-center 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="M5 13l4 4L19 7" />
                    </svg>
                    Saved
                  </span>
                )}
                <button type="submit" className="btn btn-primary btn-sm" disabled={savingSite}>
                  {savingSite ? <span className="loading loading-spinner loading-sm" /> : 'Save'}
                </button>
              </div>
            </div>
          </form>

          {/* AI Settings */}
          <form onSubmit={handleAISubmit} className="card bg-base-200/50 border border-base-300">
            <div className="card-body">
              <div className="flex items-center gap-2">
                <h3 className="card-title text-lg">AI Assistant</h3>
                {aiConfigured ? (
                  <span className="badge badge-soft badge-success badge-sm">Connected</span>
                ) : (
                  <span className="badge badge-soft badge-warning badge-sm">Not configured</span>
                )}
              </div>
              <p className="text-sm text-base-content/50 -mt-2 mb-4">Configure the AI that powers your assistant</p>

              <div className="space-y-4">
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Provider</span>
                  </label>
                  <select
                    name="ai_provider"
                    className="select select-bordered w-full"
                    value={provider}
                    onChange={handleProviderChange}
                  >
                    <option value="anthropic">Anthropic (Claude)</option>
                    <option value="openai">OpenAI (GPT)</option>
                    <option value="mock">Mock (Testing)</option>
                  </select>
                </div>

                {provider !== 'mock' && (
                  <div className="form-control">
                    <label className="label">
                      <span className="label-text">Model</span>
                    </label>
                    <select
                      name="ai_model"
                      className="select select-bordered w-full"
                      defaultValue={settings?.aiModel || getModelOptions()[0].value}
                    >
                      {getModelOptions().map((model) => (
                        <option key={model.value} value={model.value}>{model.label}</option>
                      ))}
                    </select>
                  </div>
                )}

                {provider !== 'mock' && (
                  <div className="form-control">
                    <label className="floating-label">
                      <span>API Key</span>
                      <input
                        type="password"
                        name="ai_api_key"
                        placeholder={aiConfigured ? '••••••••••••••••' : 'sk-...'}
                        className="input input-bordered w-full"
                      />
                    </label>
                    <label className="label">
                      <span className="label-text-alt text-base-content/50">
                        {aiConfigured ? 'Leave empty to keep current key' : 'Get your API key from the provider dashboard'}
                      </span>
                    </label>
                  </div>
                )}
              </div>

              <div className="card-actions justify-end mt-4">
                {aiSuccess && (
                  <span className="text-success text-sm flex items-center 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="M5 13l4 4L19 7" />
                    </svg>
                    Saved
                  </span>
                )}
                <button type="submit" className="btn btn-primary btn-sm" disabled={savingAI}>
                  {savingAI ? <span className="loading loading-spinner loading-sm" /> : 'Save'}
                </button>
              </div>
            </div>
          </form>

          {/* Data Management */}
          <div className="card bg-base-200/50 border border-base-300">
            <div className="card-body">
              <h3 className="card-title text-lg">Data</h3>
              <p className="text-sm text-base-content/50 -mt-2 mb-4">Backup and restore your site</p>

              <div className="flex items-center justify-between p-4 bg-base-300/50 rounded-lg">
                <div>
                  <p className="font-medium">Download Backup</p>
                  <p className="text-sm text-base-content/50">Export all pages, partials, collections, and settings as JSON</p>
                </div>
                <a href="/admin/backup" className="btn btn-primary btn-sm gap-2" download>
                  <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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
                  </svg>
                  Download
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
← Back