readysite / website / frontend / pages / Files.jsx
21.1 KB
Files.jsx
import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { apiGet, apiPatch, apiDelete, uploadFile } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';
import { TiltCard } from '../components/TiltCard.jsx';

export function Files() {
  const { toggleChat } = useChat() || {};
  const [files, setFiles] = useState(null);
  const [loading, setLoading] = useState(true);
  const [uploading, setUploading] = useState(false);
  const [showModal, setShowModal] = useState(false);
  const [preview, setPreview] = useState(null);
  const [editFile, setEditFile] = useState(null);
  const [saving, setSaving] = useState(false);

  const fetchFiles = useCallback(() => {
    setLoading(true);
    apiGet('/api/files')
      .then(setFiles)
      .finally(() => setLoading(false));
  }, []);

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

  const handleUpload = async (e) => {
    e.preventDefault();
    const input = e.target.querySelector('input[type="file"]');
    if (!input.files?.length) return;

    setUploading(true);
    try {
      await uploadFile(input.files[0]);
      setShowModal(false);
      setPreview(null);
      fetchFiles();
    } finally {
      setUploading(false);
    }
  };

  const handleFileSelect = (e) => {
    const file = e.target.files?.[0];
    if (!file) {
      setPreview(null);
      return;
    }

    const preview = {
      name: file.name,
      size: formatSize(file.size),
      isImage: file.type.startsWith('image/'),
    };

    if (preview.isImage) {
      const reader = new FileReader();
      reader.onload = (e) => {
        preview.dataUrl = e.target.result;
        setPreview({ ...preview });
      };
      reader.readAsDataURL(file);
    }

    setPreview(preview);
  };

  const deleteFile = async (id) => {
    if (!confirm('Delete this file?')) return;
    await apiDelete(`/api/files/${id}`);
    fetchFiles();
  };

  const openEdit = (file) => {
    setEditFile(file);
  };

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

    setSaving(true);
    try {
      await apiPatch(`/api/files/${editFile.id}`, {
        path: formData.get('path') || '',
        published: formData.get('published') === 'on',
      });
      setEditFile(null);
      fetchFiles();
    } finally {
      setSaving(false);
    }
  };

  const formatSize = (bytes) => {
    if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
    if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
    return bytes + ' bytes';
  };

  const totalSize = files?.items?.reduce((sum, f) => sum + (f.size || 0), 0) || 0;

  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">Files</h2>
          <p className="text-xs text-base-content/50">Uploaded media</p>
        </div>
        <div className="flex items-center gap-2">
          <button onClick={() => setShowModal(true)} 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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
            </svg>
            Upload
          </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 p-4">
        {loading ? (
          <div className="flex justify-center py-12">
            <span className="loading loading-spinner loading-lg" />
          </div>
        ) : files?.items?.length ? (
          <div className="space-y-2 max-w-2xl mx-auto">
            {files.items.map((file) => (
              <TiltCard
                key={file.id}
                onClick={() => openEdit(file)}
                className={`block rounded-lg bg-base-100/80 backdrop-blur-sm border border-white/5 hover:shadow-lg hover:border-white/10 cursor-pointer border-l-4 ${file.published ? 'border-l-success' : 'border-l-warning'}`}
              >
                <div className="p-3">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-3">
                      {file.mimeType?.startsWith('image/') ? (
                        <div className="w-10 h-10 rounded-lg bg-base-200 flex items-center justify-center flex-shrink-0 overflow-hidden">
                          <img src={`/api/files/${file.id}`} alt={file.name} className="w-full h-full object-cover" />
                        </div>
                      ) : file.mimeType === 'application/pdf' ? (
                        <div className="w-10 h-10 rounded-lg bg-error/15 flex items-center justify-center flex-shrink-0">
                          <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
                          </svg>
                        </div>
                      ) : file.mimeType?.startsWith('text/') ? (
                        <div className="w-10 h-10 rounded-lg bg-info/15 flex items-center justify-center flex-shrink-0">
                          <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-info" 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 className="w-10 h-10 rounded-lg bg-base-200 flex items-center justify-center flex-shrink-0">
                          <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
                          </svg>
                        </div>
                      )}
                      <div className="min-w-0">
                        <span className="font-semibold text-base-content truncate block">{file.name}</span>
                        <div className="text-xs text-base-content/40">
                          {formatSize(file.size)}
                          {file.path && <span className="font-mono"> ยท /_file/{file.path}</span>}
                        </div>
                      </div>
                    </div>
                    <div className="flex items-center gap-2">
                      <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
                      </svg>
                    </div>
                  </div>
                </div>
              </TiltCard>
            ))}
          </div>
        ) : (
          <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 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>
            <h3 className="text-lg font-semibold mb-1">No files yet</h3>
            <p className="text-base-content/50 text-sm mb-4 max-w-sm">Upload your first file to get started.</p>
            <button onClick={() => setShowModal(true)} className="btn btn-primary btn-sm">Upload File</button>
          </div>
        )}
      </div>

      {/* Upload Modal */}
      {showModal && (
        <dialog className="modal modal-open">
          <div className="modal-box w-11/12 max-w-lg">
            <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => { setShowModal(false); setPreview(null); }}>โœ•</button>
            <h3 className="font-bold text-lg mb-4">Upload File</h3>

            <form onSubmit={handleUpload} className="space-y-4">
              <div className="form-control">
                <label className="label">
                  <span className="label-text">Select File</span>
                  <span className="label-text-alt text-base-content/50">Max 10MB</span>
                </label>
                <input
                  type="file"
                  className="file-input file-input-bordered w-full"
                  required
                  onChange={handleFileSelect}
                />
              </div>

              {preview && (
                <div className="rounded-lg bg-base-200 p-4">
                  <div className="flex items-center gap-3">
                    <div className="w-12 h-12 rounded-lg bg-base-300 flex items-center justify-center flex-shrink-0">
                      {preview.isImage ? (
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-success" 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>
                      ) : (
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
                        </svg>
                      )}
                    </div>
                    <div className="min-w-0">
                      <p className="font-medium truncate">{preview.name}</p>
                      <p className="text-sm text-base-content/50">{preview.size}</p>
                    </div>
                  </div>
                  {preview.dataUrl && (
                    <div className="mt-3">
                      <img src={preview.dataUrl} className="max-h-48 rounded-lg mx-auto" alt="Preview" />
                    </div>
                  )}
                </div>
              )}

              <p className="text-xs text-base-content/50">
                Supported: Images (PNG, JPEG, GIF, WebP, SVG), PDFs, text files, and more.
              </p>

              <div className="flex justify-end gap-2 pt-4">
                <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setShowModal(false); setPreview(null); }}>Cancel</button>
                <button type="submit" className="btn btn-primary btn-sm" disabled={uploading}>
                  {uploading ? <span className="loading loading-spinner loading-sm" /> : (
                    <>
                      <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-8l-4-4m0 0L8 8m4-4v12" />
                      </svg>
                      Upload
                    </>
                  )}
                </button>
              </div>
            </form>
          </div>
          <div className="modal-backdrop" onClick={() => { setShowModal(false); setPreview(null); }} />
        </dialog>
      )}

      {/* Edit File Modal */}
      {editFile && (
        <dialog className="modal modal-open">
          <div className="modal-box w-11/12 max-w-2xl max-h-[90vh]">
            <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => setEditFile(null)}>โœ•</button>
            <h3 className="font-bold text-lg mb-4">Edit File</h3>

            <div className="space-y-6">
              {/* File Preview */}
              <div className="card bg-base-200">
                <div className="card-body p-4">
                  <h4 className="text-sm font-medium mb-2">Preview</h4>
                  <div className="flex justify-center py-4 bg-base-300 rounded-lg">
                    {editFile.mimeType?.startsWith('image/') ? (
                      <img src={`/api/files/${editFile.id}`} alt={editFile.name} className="max-h-48 max-w-full rounded" />
                    ) : editFile.mimeType === 'application/pdf' ? (
                      <div className="flex flex-col items-center gap-2 py-4">
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 text-error/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
                        </svg>
                        <span className="text-sm text-base-content/50">PDF Document</span>
                      </div>
                    ) : editFile.mimeType?.startsWith('text/') ? (
                      <div className="flex flex-col items-center gap-2 py-4">
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 text-info/60" 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>
                        <span className="text-sm text-base-content/50">Text File</span>
                      </div>
                    ) : (
                      <div className="flex flex-col items-center gap-2 py-4">
                        <svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
                        </svg>
                        <span className="text-sm text-base-content/50">File</span>
                      </div>
                    )}
                  </div>
                </div>
              </div>

              {/* Publishing Settings Form */}
              <form onSubmit={handleEditSubmit} className="card bg-base-200">
                <div className="card-body p-4 space-y-4">
                  <h4 className="text-sm font-medium">Publishing Settings</h4>

                  <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={editFile.published} />
                      <div>
                        <span className="label-text font-medium">Published</span>
                        <p className="text-xs text-base-content/50">Make this file publicly accessible at a custom path</p>
                      </div>
                    </label>
                  </div>

                  <div className="form-control">
                    <label className="label">
                      <span className="label-text font-medium">Public Path</span>
                    </label>
                    <div className="join w-full">
                      <span className="join-item btn btn-sm btn-disabled font-mono text-xs">/_file/</span>
                      <input type="text" name="path" placeholder="images/logo.png"
                             className="input input-bordered input-sm join-item w-full font-mono"
                             defaultValue={editFile.path || ''} />
                    </div>
                    <label className="label">
                      <span className="label-text-alt text-base-content/50">Path where the file will be accessible (without leading slash)</span>
                    </label>
                  </div>

                  {editFile.published && editFile.path && (
                    <div className="alert alert-success">
                      <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
                      </svg>
                      <div>
                        <p className="font-medium text-sm">Public URL</p>
                        <a href={`/_file/${editFile.path}`} target="_blank" className="link link-hover font-mono text-xs">/_file/{editFile.path}</a>
                      </div>
                    </div>
                  )}

                  <div className="pt-2 flex justify-between">
                    <button
                      type="button"
                      className="btn btn-ghost btn-sm text-error"
                      onClick={() => { deleteFile(editFile.id); setEditFile(null); }}
                    >
                      Delete
                    </button>
                    <div className="flex gap-2">
                      <button type="button" className="btn btn-ghost btn-sm" onClick={() => setEditFile(null)}>Cancel</button>
                      <button type="submit" className="btn btn-primary btn-sm" disabled={saving}>
                        {saving ? <span className="loading loading-spinner loading-sm" /> : 'Save Changes'}
                      </button>
                    </div>
                  </div>
                </div>
              </form>

              {/* File Information */}
              <div className="card bg-base-200">
                <div className="card-body p-4">
                  <h4 className="text-sm font-medium mb-2">File Information</h4>
                  <div className="text-sm space-y-2">
                    <div className="flex justify-between">
                      <span className="text-base-content/50">ID</span>
                      <code className="bg-base-300 px-2 py-0.5 rounded text-xs">{editFile.id}</code>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-base-content/50">Original Name</span>
                      <span>{editFile.name}</span>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-base-content/50">Size</span>
                      <span>{formatSize(editFile.size)}</span>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-base-content/50">MIME Type</span>
                      <code className="bg-base-300 px-2 py-0.5 rounded text-xs">{editFile.mimeType}</code>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-base-content/50">Direct URL</span>
                      <a href={`/api/files/${editFile.id}`} target="_blank" className="link link-hover font-mono text-xs">/api/files/{editFile.id}</a>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-base-content/50">Uploaded</span>
                      <span>{new Date(editFile.created).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div className="modal-backdrop" onClick={() => setEditFile(null)} />
        </dialog>
      )}
    </div>
  );
}
โ† Back