readysite / website / frontend / pages / Dashboard.jsx
14.9 KB
Dashboard.jsx
import * as React from 'react';
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { apiGet } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';
import { TiltCard } from '../components/TiltCard.jsx';

export function Dashboard() {
  const { toggleChat } = useChat() || {};
  const [pages, setPages] = useState(null);
  const [collections, setCollections] = useState([]);
  const [files, setFiles] = useState(null);
  const [users, setUsers] = useState(null);
  const [settings, setSettings] = useState(null);
  const [loading, setLoading] = useState(true);
  const eventSourceRef = useRef(null);

  const loadData = () => {
    return Promise.all([
      apiGet('/api/admin/pages'),
      apiGet('/api/admin/collections'),
      apiGet('/api/files'),
      apiGet('/api/users'),
      apiGet('/api/admin/settings'),
    ]).then(([pagesData, collectionsData, filesData, usersData, settingsData]) => {
      setPages(pagesData);
      setCollections(collectionsData || []);
      setFiles(filesData);
      setUsers(usersData);
      setSettings(settingsData);
      setLoading(false);
    }).catch(() => setLoading(false));
  };

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

  // Subscribe to real-time events
  useEffect(() => {
    const eventSource = new EventSource('/api/admin/events/subscribe');
    eventSourceRef.current = eventSource;

    // Page events
    eventSource.addEventListener('page_create', (e) => {
      try {
        const data = JSON.parse(e.data);
        setPages(prev => prev ? {
          ...prev,
          totalItems: (prev.totalItems || 0) + 1,
          items: [data, ...(prev.items || [])].slice(0, 10)
        } : prev);
      } catch {}
    });

    eventSource.addEventListener('page_update', (e) => {
      try {
        const data = JSON.parse(e.data);
        setPages(prev => {
          if (!prev?.items) return prev;
          const updated = prev.items.map(p => p.id === data.id ? { ...p, ...data } : p);
          return { ...prev, items: updated };
        });
      } catch {}
    });

    eventSource.addEventListener('page_delete', (e) => {
      try {
        const data = JSON.parse(e.data);
        setPages(prev => prev ? {
          ...prev,
          totalItems: Math.max(0, (prev.totalItems || 0) - 1),
          items: (prev.items || []).filter(p => p.id !== data.id)
        } : prev);
      } catch {}
    });

    // Collection events
    eventSource.addEventListener('collection_create', (e) => {
      try {
        const data = JSON.parse(e.data);
        setCollections(prev => [data, ...prev]);
      } catch {}
    });

    eventSource.addEventListener('collection_update', (e) => {
      try {
        const data = JSON.parse(e.data);
        setCollections(prev => prev.map(c =>
          c.id === data.collectionId ? { ...c, ...data } : c
        ));
      } catch {}
    });

    eventSource.addEventListener('collection_delete', (e) => {
      try {
        const data = JSON.parse(e.data);
        setCollections(prev => prev.filter(c => c.id !== data.collectionId));
      } catch {}
    });

    // Document events
    eventSource.addEventListener('document_create', (e) => {
      try {
        const data = JSON.parse(e.data);
        // Update document count for the collection
        setCollections(prev => prev.map(c =>
          c.id === data.collectionId
            ? { ...c, documentCount: (c.documentCount || 0) + 1 }
            : c
        ));
      } catch {}
    });

    eventSource.addEventListener('document_delete', (e) => {
      try {
        const data = JSON.parse(e.data);
        setCollections(prev => prev.map(c =>
          c.id === data.collectionId
            ? { ...c, documentCount: Math.max(0, (c.documentCount || 0) - 1) }
            : c
        ));
      } catch {}
    });

    eventSource.onerror = () => {
      // Reconnect after a delay on error
      setTimeout(() => {
        if (eventSourceRef.current) {
          eventSourceRef.current.close();
          eventSourceRef.current = new EventSource('/api/admin/events/subscribe');
        }
      }, 5000);
    };

    return () => {
      eventSource.close();
      eventSourceRef.current = null;
    };
  }, []);

  const siteName = settings?.siteName || 'ReadySite';
  const aiConfigured = settings?.aiProvider && settings?.aiProvider !== 'mock';

  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">
        <h2 className="font-semibold">Welcome to {siteName}</h2>
        {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>

      {/* Content */}
      <div className="flex-1 overflow-auto p-6">
        <div className="max-w-2xl mx-auto">
          {/* Welcome Message */}
          <div className="text-center mb-8">
            <div className="text-5xl mb-4">&#128075;</div>
            <h1 className="text-2xl font-bold mb-2">Welcome to your workspace</h1>
            <p className="text-base-content/70">
              Select a page or collection to preview, or start a conversation with the AI assistant.
            </p>
          </div>

          {/* Quick Stats - 3 column grid */}
          <div className="grid grid-cols-3 gap-4 mb-8">
            <TiltCard className="card bg-base-200/50 border border-base-300 hover:bg-base-200 rounded-2xl">
              <Link to="/pages" className="card-body p-4">
                <div className="flex items-center gap-3">
                  <div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" 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>
                  </div>
                  <div>
                    <p className="text-2xl font-bold">{loading ? '-' : (pages?.totalItems ?? 0)}</p>
                    <p className="text-xs text-base-content/70">Pages</p>
                  </div>
                </div>
              </Link>
            </TiltCard>
            <TiltCard className="card bg-base-200/50 border border-base-300 hover:bg-base-200 rounded-2xl">
              <Link to="/collections" className="card-body p-4">
                <div className="flex items-center gap-3">
                  <div className="w-12 h-12 rounded-lg bg-secondary/10 flex items-center justify-center">
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
                    </svg>
                  </div>
                  <div>
                    <p className="text-2xl font-bold">{loading ? '-' : collections.length}</p>
                    <p className="text-xs text-base-content/70">Collections</p>
                  </div>
                </div>
              </Link>
            </TiltCard>
            <TiltCard className="card bg-base-200/50 border border-base-300 hover:bg-base-200 rounded-2xl">
              <Link to="/files" className="card-body p-4">
                <div className="flex items-center gap-3">
                  <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center">
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
                    </svg>
                  </div>
                  <div>
                    <p className="text-2xl font-bold">{loading ? '-' : (files?.totalItems ?? 0)}</p>
                    <p className="text-xs text-base-content/70">Files</p>
                  </div>
                </div>
              </Link>
            </TiltCard>
          </div>

          {/* AI Configuration Warning */}
          {!loading && !aiConfigured && (
            <div className="flex items-center gap-4 p-4 mb-8 rounded-lg bg-warning/10 border border-warning/30">
              <div className="w-10 h-10 rounded-lg bg-warning/20 flex items-center justify-center flex-shrink-0">
                <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
                </svg>
              </div>
              <div className="flex-1">
                <p className="font-medium text-warning">AI not configured</p>
                <p className="text-sm text-base-content/60">Add your API key to enable the AI assistant for content creation.</p>
              </div>
              <Link to="/settings" className="btn btn-warning btn-sm gap-1 flex-shrink-0">
                <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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
                </svg>
                Configure AI
              </Link>
            </div>
          )}

          {/* Pages Section */}
          {!loading && pages?.items?.length > 0 && (
            <div className="mb-6">
              <div className="flex items-center justify-between mb-3">
                <h3 className="text-sm font-medium text-base-content/70 flex items-center 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 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>
                  Recent Pages
                </h3>
                <Link to="/pages" className="text-xs text-primary hover:underline">View All</Link>
              </div>
              <div className="space-y-2">
                {pages.items.slice(0, 5).map((page) => (
                  <TiltCard key={page.id} className="rounded-lg bg-base-200/50 border border-base-300 hover:bg-base-200">
                    <Link
                      to={`/pages/${page.id}`}
                      className="flex items-center justify-between p-3"
                    >
                      <span className="font-medium">{page.title}</span>
                      <div className="flex items-center gap-2">
                        {page.published ? (
                          <span className="badge badge-soft badge-success badge-xs">Published</span>
                        ) : (
                          <span className="badge badge-soft badge-warning badge-xs">Draft</span>
                        )}
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
                        </svg>
                      </div>
                    </Link>
                  </TiltCard>
                ))}
              </div>
            </div>
          )}

          {/* Collections Section */}
          {!loading && collections.length > 0 && (
            <div className="mb-6">
              <div className="flex items-center justify-between mb-3">
                <h3 className="text-sm font-medium text-base-content/70 flex items-center 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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
                  </svg>
                  Collections
                </h3>
                <Link to="/collections" className="text-xs text-primary hover:underline">View All</Link>
              </div>
              <div className="space-y-2">
                {collections.slice(0, 5).map((col) => (
                  <TiltCard key={col.id} className="rounded-lg bg-base-200/50 border border-base-300 hover:bg-base-200">
                    <Link
                      to={`/collections/${col.id}`}
                      className="flex items-center justify-between p-3"
                    >
                      <span className="font-medium">{col.name}</span>
                      <div className="flex items-center gap-2">
                        <span className="badge badge-soft badge-info badge-xs">{col.documentCount || 0} docs</span>
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
                        </svg>
                      </div>
                    </Link>
                  </TiltCard>
                ))}
              </div>
            </div>
          )}


        </div>
      </div>
    </div>
  );
}
← Back