mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(files): add Files page with nav, list, details, upload, and routing
Creates the full Files page UI (Tasks 8 & 9): FilesNav, FileListItem, FileList, FileDetails, FileUploadArea components, FilesPage layout, and wires up the /files route plus sidebar/mobile-nav entries.
This commit is contained in:
@@ -4,6 +4,7 @@ import { HomePage } from "./pages/home-page";
|
|||||||
import { LoginPage } from "./pages/login-page";
|
import { LoginPage } from "./pages/login-page";
|
||||||
import { ToolPage } from "./pages/tool-page";
|
import { ToolPage } from "./pages/tool-page";
|
||||||
import { AutomatePage } from "./pages/automate-page";
|
import { AutomatePage } from "./pages/automate-page";
|
||||||
|
import { FilesPage } from "./pages/files-page";
|
||||||
import { FullscreenGridPage } from "./pages/fullscreen-grid-page";
|
import { FullscreenGridPage } from "./pages/fullscreen-grid-page";
|
||||||
import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider";
|
import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider";
|
||||||
import { useAuth } from "./hooks/use-auth";
|
import { useAuth } from "./hooks/use-auth";
|
||||||
@@ -87,6 +88,7 @@ export function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/automate" element={<AutomatePage />} />
|
<Route path="/automate" element={<AutomatePage />} />
|
||||||
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
||||||
<Route path="/:toolId" element={<ToolPage />} />
|
<Route path="/:toolId" element={<ToolPage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FileImage } from "lucide-react";
|
||||||
|
import { TOOLS } from "@stirling-image/shared";
|
||||||
|
import {
|
||||||
|
apiGetFileDetails,
|
||||||
|
getFileThumbnailUrl,
|
||||||
|
getFileDownloadUrl,
|
||||||
|
type UserFileDetail,
|
||||||
|
} from "@/lib/api";
|
||||||
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
import { useFileStore } from "@/stores/file-store";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function toolName(toolId: string): string {
|
||||||
|
return TOOLS.find((t) => t.id === toolId)?.name ?? toolId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileDetailsProps {
|
||||||
|
mobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileDetails({ mobile = false }: FileDetailsProps) {
|
||||||
|
const { selectedFileId } = useFilesPageStore();
|
||||||
|
const setFiles = useFileStore((s) => s.setFiles);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [details, setDetails] = useState<UserFileDetail | null>(null);
|
||||||
|
const [loadingDetails, setLoadingDetails] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedFileId) {
|
||||||
|
setDetails(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingDetails(true);
|
||||||
|
apiGetFileDetails(selectedFileId)
|
||||||
|
.then(setDetails)
|
||||||
|
.catch(() => setDetails(null))
|
||||||
|
.finally(() => setLoadingDetails(false));
|
||||||
|
}, [selectedFileId]);
|
||||||
|
|
||||||
|
async function handleOpenFile() {
|
||||||
|
if (!details) return;
|
||||||
|
const res = await fetch(getFileDownloadUrl(details.id), {
|
||||||
|
headers: { Authorization: `Bearer ${localStorage.getItem("stirling-token") || ""}` },
|
||||||
|
});
|
||||||
|
if (!res.ok) return;
|
||||||
|
const blob = await res.blob();
|
||||||
|
const file = new File([blob], details.originalName, { type: details.mimeType });
|
||||||
|
setFiles([file]);
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedFileId) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center text-muted-foreground",
|
||||||
|
mobile ? "flex-1" : "w-72 shrink-0 border-l border-border",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileImage className="h-12 w-12 mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">Select a file to view details</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingDetails) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center",
|
||||||
|
mobile ? "flex-1" : "w-72 shrink-0 border-l border-border",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="h-6 w-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col overflow-y-auto",
|
||||||
|
mobile ? "flex-1" : "w-72 shrink-0 border-l border-border",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div className="p-4 border-b border-border">
|
||||||
|
<img
|
||||||
|
src={getFileThumbnailUrl(details.id)}
|
||||||
|
alt={details.originalName}
|
||||||
|
className="w-full rounded-lg object-contain max-h-48 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details card */}
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div className="rounded-lg border border-border overflow-hidden">
|
||||||
|
<div className="bg-blue-500/10 border-b border-border px-3 py-2">
|
||||||
|
<h4 className="text-sm font-semibold text-blue-600 dark:text-blue-400">File Details</h4>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
<DetailRow label="Name" value={details.originalName} />
|
||||||
|
<DetailRow label="Format" value={details.mimeType.replace("image/", "").toUpperCase()} />
|
||||||
|
<DetailRow label="Size" value={formatSize(details.size)} />
|
||||||
|
<DetailRow
|
||||||
|
label="Dimensions"
|
||||||
|
value={
|
||||||
|
details.width && details.height
|
||||||
|
? `${details.width} × ${details.height}`
|
||||||
|
: "—"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailRow label="Version" value={`V${details.version}`} />
|
||||||
|
<DetailRow
|
||||||
|
label="Tools Used"
|
||||||
|
value={
|
||||||
|
details.toolChain.length > 0
|
||||||
|
? details.toolChain.map(toolName).join(", ")
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Open File button */}
|
||||||
|
<div className="p-4 border-t border-border">
|
||||||
|
<button
|
||||||
|
onClick={handleOpenFile}
|
||||||
|
className="w-full px-4 py-2 bg-primary text-primary-foreground text-sm font-medium rounded-lg hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
Open File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-start gap-2 px-3 py-2">
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">{label}</span>
|
||||||
|
<span className="text-xs text-foreground text-right break-all">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { TOOLS } from "@stirling-image/shared";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { UserFile } from "@/lib/api";
|
||||||
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
return new Date(dateStr).toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolName(toolId: string): string {
|
||||||
|
return TOOLS.find((t) => t.id === toolId)?.name ?? toolId;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileListItemProps {
|
||||||
|
file: UserFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileListItem({ file }: FileListItemProps) {
|
||||||
|
const { selectedFileId, checkedIds, selectFile, toggleChecked } = useFilesPageStore();
|
||||||
|
const isSelected = selectedFileId === file.id;
|
||||||
|
const isChecked = checkedIds.has(file.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => selectFile(file.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary/10 border border-primary/30"
|
||||||
|
: "hover:bg-muted border border-transparent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={() => toggleChecked(file.id)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="h-4 w-4 shrink-0 accent-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* File name */}
|
||||||
|
<span className="flex-1 min-w-0 text-sm font-medium text-foreground truncate">
|
||||||
|
{file.originalName}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Tool chain */}
|
||||||
|
{file.toolChain.length > 0 && (
|
||||||
|
<span className="hidden md:block text-xs text-primary shrink-0">
|
||||||
|
{file.toolChain.map(toolName).join(" → ")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Version badge */}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[10px] px-1.5 py-0.5 rounded font-medium shrink-0",
|
||||||
|
file.version >= 2
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-muted text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
V{file.version}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Size */}
|
||||||
|
<span className="hidden sm:block text-xs text-muted-foreground shrink-0 w-16 text-right">
|
||||||
|
{formatSize(file.size)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<span className="hidden lg:block text-xs text-muted-foreground shrink-0 w-24 text-right">
|
||||||
|
{formatDate(file.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Search, Trash2, Download } from "lucide-react";
|
||||||
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
import { getFileDownloadUrl } from "@/lib/api";
|
||||||
|
import { FileListItem } from "./file-list-item";
|
||||||
|
|
||||||
|
export function FileList() {
|
||||||
|
const {
|
||||||
|
files,
|
||||||
|
checkedIds,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchFiles,
|
||||||
|
deleteChecked,
|
||||||
|
toggleCheckAll,
|
||||||
|
setSearchQuery,
|
||||||
|
} = useFilesPageStore();
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFiles();
|
||||||
|
}, [fetchFiles]);
|
||||||
|
|
||||||
|
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const val = e.target.value;
|
||||||
|
setInputValue(val);
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
setSearchQuery(val);
|
||||||
|
fetchFiles();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBulkDownload() {
|
||||||
|
for (const id of checkedIds) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = getFileDownloadUrl(id);
|
||||||
|
a.download = "";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChecked = files.length > 0 && checkedIds.size === files.length;
|
||||||
|
const someChecked = checkedIds.size > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden border-r border-border">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="p-3 border-b border-border">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search files..."
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
className="w-full pl-8 pr-3 py-1.5 text-sm bg-muted rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground placeholder:text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allChecked}
|
||||||
|
onChange={toggleCheckAll}
|
||||||
|
className="h-4 w-4 accent-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground flex-1">
|
||||||
|
{someChecked ? `${checkedIds.size} selected` : `${files.length} files`}
|
||||||
|
</span>
|
||||||
|
{someChecked && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={deleteChecked}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleBulkDownload}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-foreground hover:bg-muted rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File list */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 space-y-0.5">
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<div className="h-6 w-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && error && (
|
||||||
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<p className="text-sm text-destructive">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && !error && files.length === 0 && (
|
||||||
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<p className="text-sm text-muted-foreground">No files found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && !error && files.map((file) => (
|
||||||
|
<FileListItem key={file.id} file={file} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Upload } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
|
||||||
|
export function FileUploadArea() {
|
||||||
|
const { uploadFiles, loading } = useFilesPageStore();
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
function handleDragOver(e: React.DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragging(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave() {
|
||||||
|
setDragging(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e: React.DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
const files = Array.from(e.dataTransfer.files).filter((f) =>
|
||||||
|
f.type.startsWith("image/"),
|
||||||
|
);
|
||||||
|
if (files.length > 0) uploadFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const files = Array.from(e.target.files ?? []);
|
||||||
|
if (files.length > 0) uploadFiles(files);
|
||||||
|
// Reset input so the same file can be re-selected
|
||||||
|
e.target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center p-8">
|
||||||
|
<div
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => !loading && inputRef.current?.click()}
|
||||||
|
className={cn(
|
||||||
|
"w-full max-w-lg flex flex-col items-center justify-center gap-4 p-12 rounded-xl border-2 border-dashed transition-colors cursor-pointer",
|
||||||
|
dragging
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "border-border hover:border-primary/50 hover:bg-muted/50",
|
||||||
|
loading && "pointer-events-none opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="h-10 w-10 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Upload className="h-10 w-10 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{loading ? "Uploading..." : "Drop images here"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
or click to select files
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Clock, Upload, Cloud } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
|
||||||
|
export function FilesNav() {
|
||||||
|
const { activeTab, setActiveTab } = useFilesPageStore();
|
||||||
|
const items = [
|
||||||
|
{ id: "recent" as const, label: "Recent", icon: Clock },
|
||||||
|
{ id: "upload" as const, label: "Upload Files", icon: Upload },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-48 border-r border-border p-4 shrink-0 hidden md:block">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-3">My Files</h3>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => setActiveTab(item.id)}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors",
|
||||||
|
activeTab === item.id
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="h-4 w-4" />
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground/40 cursor-not-allowed">
|
||||||
|
<Cloud className="h-4 w-4" />
|
||||||
|
Google Drive
|
||||||
|
<span className="text-[10px] bg-muted px-1.5 py-0.5 rounded ml-auto">Soon</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,10 +3,10 @@ import { cn } from "@/lib/utils";
|
|||||||
import {
|
import {
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Workflow,
|
Workflow,
|
||||||
FolderOpen,
|
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Settings,
|
Settings,
|
||||||
Grid3x3,
|
Grid3x3,
|
||||||
|
FolderOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { AppLayout } from "@/components/layout/app-layout";
|
||||||
|
import { FilesNav } from "@/components/files/files-nav";
|
||||||
|
import { FileList } from "@/components/files/file-list";
|
||||||
|
import { FileDetails } from "@/components/files/file-details";
|
||||||
|
import { FileUploadArea } from "@/components/files/file-upload-area";
|
||||||
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
|
||||||
|
export function FilesPage() {
|
||||||
|
const { activeTab } = useFilesPageStore();
|
||||||
|
return (
|
||||||
|
<AppLayout showToolPanel={false}>
|
||||||
|
<div className="flex h-full w-full overflow-hidden">
|
||||||
|
<FilesNav />
|
||||||
|
{activeTab === "recent" ? (
|
||||||
|
<>
|
||||||
|
<FileList />
|
||||||
|
<FileDetails />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FileUploadArea />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user