feat: enhance frontend with new UI components and Tailwind CSS integration
- Added CreateButton and DocumentCard components for document management. - Implemented tabbed interface for owned and shared documents in Home page. - Integrated Tailwind CSS for styling and layout improvements. - Introduced utility functions for class name management. - Updated package.json with new dependencies for UI components and styling. - Created PostCSS configuration for Tailwind CSS. - Refactored Navbar and button components for better usability and design. - Enhanced document API to include owner_id for document sharing functionality.
This commit is contained in:
@@ -4,6 +4,7 @@ export type DocumentType = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "editor" | "kanban";
|
||||
owner_id?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
55
frontend/src/components/Home/CreateButton.tsx
Normal file
55
frontend/src/components/Home/CreateButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus, FileText, KanbanSquare } from 'lucide-react';
|
||||
|
||||
interface CreateButtonProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
icon: 'plus' | 'document' | 'kanban';
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
plus: Plus,
|
||||
document: FileText,
|
||||
kanban: KanbanSquare,
|
||||
};
|
||||
|
||||
export function CreateButton({ onClick, disabled, icon, children }: CreateButtonProps) {
|
||||
const Icon = iconMap[icon];
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className="
|
||||
bg-pixel-purple-bright
|
||||
hover:bg-pixel-purple-deep
|
||||
text-white
|
||||
border-[3px]
|
||||
border-pixel-outline
|
||||
shadow-pixel
|
||||
hover:shadow-pixel-hover
|
||||
hover:-translate-y-0.5
|
||||
hover:-translate-x-0.5
|
||||
active:translate-y-0.5
|
||||
active:translate-x-0.5
|
||||
active:shadow-pixel-active
|
||||
transition-all
|
||||
duration-75
|
||||
font-sans
|
||||
font-semibold
|
||||
px-6
|
||||
py-3
|
||||
flex
|
||||
items-center
|
||||
gap-2
|
||||
disabled:opacity-60
|
||||
disabled:cursor-not-allowed
|
||||
"
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
129
frontend/src/components/Home/DocumentCard.tsx
Normal file
129
frontend/src/components/Home/DocumentCard.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { DocumentType } from '@/api/document';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { FileText, KanbanSquare, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface DocumentCardProps {
|
||||
doc: DocumentType;
|
||||
onDelete: (id: string) => void;
|
||||
isShared?: boolean;
|
||||
}
|
||||
|
||||
export function DocumentCard({ doc, onDelete, isShared }: DocumentCardProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const Icon = doc.type === 'editor' ? FileText : KanbanSquare;
|
||||
const typeLabel = doc.type === 'editor' ? 'Text Document' : 'Kanban Board';
|
||||
|
||||
const handleOpen = () => {
|
||||
navigate(`/${doc.type}/${doc.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="
|
||||
group
|
||||
relative
|
||||
bg-pixel-white
|
||||
border-[3px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-md
|
||||
hover:shadow-pixel-lg
|
||||
hover:-translate-y-0.5
|
||||
hover:-translate-x-0.5
|
||||
transition-all
|
||||
duration-100
|
||||
p-6
|
||||
flex
|
||||
flex-col
|
||||
gap-4
|
||||
">
|
||||
{/* Header with icon and type */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="
|
||||
bg-pixel-cyan-bright
|
||||
p-2
|
||||
border-[2px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
">
|
||||
<Icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-pixel text-sm text-pixel-text-primary mb-1">
|
||||
{doc.name}
|
||||
</h3>
|
||||
<span className="font-sans text-xs text-pixel-text-muted">
|
||||
{typeLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{isShared && (
|
||||
<span className="
|
||||
bg-pixel-pink-vibrant
|
||||
text-white
|
||||
font-sans
|
||||
text-xs
|
||||
font-semibold
|
||||
px-2
|
||||
py-1
|
||||
border-[2px]
|
||||
border-pixel-outline
|
||||
">
|
||||
Shared
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="font-sans text-xs text-pixel-text-muted">
|
||||
Created {new Date(doc.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 mt-auto">
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
className="
|
||||
flex-1
|
||||
bg-pixel-cyan-bright
|
||||
hover:bg-pixel-purple-bright
|
||||
text-white
|
||||
border-[2px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
hover:shadow-pixel-hover
|
||||
hover:-translate-y-0.5
|
||||
hover:-translate-x-0.5
|
||||
transition-all
|
||||
duration-75
|
||||
font-sans
|
||||
font-semibold
|
||||
"
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
{!isShared && (
|
||||
<Button
|
||||
onClick={() => onDelete(doc.id)}
|
||||
variant="outline"
|
||||
className="
|
||||
border-[2px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
hover:shadow-pixel-hover
|
||||
hover:-translate-y-0.5
|
||||
hover:-translate-x-0.5
|
||||
hover:bg-red-50
|
||||
transition-all
|
||||
duration-75
|
||||
"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './Navbar.css';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LogOut } from 'lucide-react';
|
||||
|
||||
function Navbar() {
|
||||
const { user, logout } = useAuth();
|
||||
@@ -7,24 +8,68 @@ function Navbar() {
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-content">
|
||||
<div className="navbar-brand">
|
||||
<a href="/">Realtime Collab</a>
|
||||
</div>
|
||||
<nav className="
|
||||
bg-pixel-white
|
||||
border-b-[3px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
sticky
|
||||
top-0
|
||||
z-50
|
||||
">
|
||||
<div className="max-w-7xl mx-auto px-8 py-4 flex items-center justify-between">
|
||||
{/* Brand */}
|
||||
<a
|
||||
href="/"
|
||||
className="
|
||||
font-pixel
|
||||
text-xl
|
||||
text-pixel-purple-bright
|
||||
hover:text-pixel-cyan-bright
|
||||
transition-colors
|
||||
duration-200
|
||||
"
|
||||
>
|
||||
Realtime Collab
|
||||
</a>
|
||||
|
||||
<div className="navbar-user">
|
||||
{/* User Section */}
|
||||
<div className="flex items-center gap-4">
|
||||
{user.avatar_url && (
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
alt={user.name}
|
||||
className="user-avatar"
|
||||
className="
|
||||
w-10
|
||||
h-10
|
||||
border-[3px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
"
|
||||
/>
|
||||
)}
|
||||
<span className="user-name">{user.name}</span>
|
||||
<button onClick={logout} className="logout-button">
|
||||
Logout
|
||||
</button>
|
||||
<span className="font-sans font-medium text-pixel-text-primary hidden sm:inline">
|
||||
{user.name}
|
||||
</span>
|
||||
<Button
|
||||
onClick={logout}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="
|
||||
border-[3px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
hover:shadow-pixel-hover
|
||||
hover:-translate-y-0.5
|
||||
hover:-translate-x-0.5
|
||||
transition-all
|
||||
duration-75
|
||||
font-sans
|
||||
"
|
||||
>
|
||||
<LogOut className="w-4 h-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
53
frontend/src/components/ui/button.tsx
Normal file
53
frontend/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
78
frontend/src/components/ui/card.tsx
Normal file
78
frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
52
frontend/src/components/ui/tabs.tsx
Normal file
52
frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -1,3 +1,32 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 232 217 243;
|
||||
--foreground: 43 27 56;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 43 27 56;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 43 27 56;
|
||||
--primary: 277 42% 52%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 190 100% 50%;
|
||||
--secondary-foreground: 43 27 56;
|
||||
--muted: 276 100% 97%;
|
||||
--muted-foreground: 240 13% 40%;
|
||||
--accent: 325 100% 71%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 43 27 56;
|
||||
--input: 43 27 56;
|
||||
--ring: 190 100% 50%;
|
||||
--radius: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
|
||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { DocumentType } from "../api/document.ts";
|
||||
import { documentsApi } from "../api/document.ts";
|
||||
import Navbar from "../components/Navbar.tsx";
|
||||
import PixelIcon from "../components/PixelIcon/PixelIcon.tsx";
|
||||
import FloatingGem from "../components/PixelSprites/FloatingGem.tsx";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import type { DocumentType } from '@/api/document';
|
||||
import { documentsApi } from '@/api/document';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import { DocumentCard } from '@/components/Home/DocumentCard';
|
||||
import { CreateButton } from '@/components/Home/CreateButton';
|
||||
import FloatingGem from '@/components/PixelSprites/FloatingGem';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
const Home = () => {
|
||||
const { user } = useAuth();
|
||||
const [documents, setDocuments] = useState<DocumentType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [creating, setCreating] = useState(false);
|
||||
@@ -17,8 +21,8 @@ const Home = () => {
|
||||
const { documents } = await documentsApi.list();
|
||||
setDocuments(documents || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to load documents:", error);
|
||||
setDocuments([]); // Set empty array on error to prevent null access
|
||||
console.error('Failed to load documents:', error);
|
||||
setDocuments([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -28,87 +32,160 @@ const Home = () => {
|
||||
loadDocuments();
|
||||
}, []);
|
||||
|
||||
const createDocument = async (type: "editor" | "kanban") => {
|
||||
// Filter documents into owned and shared
|
||||
const ownedDocuments = documents.filter(doc => doc.owner_id === user?.id);
|
||||
const sharedDocuments = documents.filter(doc => doc.owner_id !== user?.id);
|
||||
|
||||
const createDocument = async (type: 'editor' | 'kanban') => {
|
||||
setCreating(true);
|
||||
try {
|
||||
const doc = await documentsApi.create({
|
||||
name: `New ${type === "editor" ? "Document" : "Kanban Board"}`,
|
||||
name: `New ${type === 'editor' ? 'Document' : 'Kanban Board'}`,
|
||||
type,
|
||||
});
|
||||
navigate(`/${type}/${doc.id}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to create document:", error);
|
||||
console.error('Failed to create document:', error);
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDocument = async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this document?")) return;
|
||||
if (!confirm('Are you sure you want to delete this document?')) return;
|
||||
|
||||
try {
|
||||
await documentsApi.delete(id);
|
||||
loadDocuments();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete document:", error);
|
||||
console.error('Failed to delete document:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading documents...</div>;
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-pixel-bg-light">
|
||||
<div className="font-pixel text-pixel-purple-bright animate-pixel-bounce">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="home-page" style={{ position: 'relative' }}>
|
||||
<div className="max-w-7xl mx-auto px-8 py-12 relative min-h-screen bg-pixel-bg-light">
|
||||
{/* Decorative floating gems */}
|
||||
<FloatingGem position={{ top: '20px', right: '40px' }} delay={0} size={40} />
|
||||
<FloatingGem position={{ top: '60px', left: '60px' }} delay={1.5} size={32} />
|
||||
<FloatingGem position={{ bottom: '100px', right: '100px' }} delay={3} size={36} />
|
||||
|
||||
<h1>My Documents</h1>
|
||||
{/* Page Header */}
|
||||
<div className="mb-12">
|
||||
<h1 className="font-pixel text-4xl text-pixel-purple-bright mb-6 tracking-wide">
|
||||
My Workspace
|
||||
</h1>
|
||||
|
||||
<div className="create-buttons">
|
||||
<button onClick={() => createDocument("editor")} disabled={creating}>
|
||||
<PixelIcon name="plus" size={20} />
|
||||
<span style={{ marginLeft: '8px' }}>New Text Document</span>
|
||||
</button>
|
||||
<button onClick={() => createDocument("kanban")} disabled={creating}>
|
||||
<PixelIcon name="plus" size={20} />
|
||||
<span style={{ marginLeft: '8px' }}>New Kanban Board</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* Create Buttons */}
|
||||
<div className="flex gap-4 mb-8 flex-wrap">
|
||||
<CreateButton
|
||||
onClick={() => createDocument('editor')}
|
||||
disabled={creating}
|
||||
icon="plus"
|
||||
>
|
||||
New Text Document
|
||||
</CreateButton>
|
||||
<CreateButton
|
||||
onClick={() => createDocument('kanban')}
|
||||
disabled={creating}
|
||||
icon="plus"
|
||||
>
|
||||
New Kanban Board
|
||||
</CreateButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="document-list">
|
||||
{documents.length === 0 ? (
|
||||
<p>No documents yet. Create one to get started!</p>
|
||||
) : (
|
||||
documents.map((doc) => (
|
||||
<div key={doc.id} className="document-card">
|
||||
<div className="doc-info">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<PixelIcon name={doc.type === 'editor' ? 'document' : 'kanban'} size={24} color="var(--pixel-purple-bright)" />
|
||||
<h3 style={{ margin: 0 }}>{doc.name}</h3>
|
||||
</div>
|
||||
<p className="doc-type">{doc.type}</p>
|
||||
<p className="doc-date">
|
||||
Created: {new Date(doc.created_at).toLocaleDateString()}
|
||||
{/* Tabbed Interface */}
|
||||
<Tabs defaultValue="owned" className="w-full">
|
||||
<TabsList className="
|
||||
bg-pixel-panel
|
||||
border-[3px]
|
||||
border-pixel-outline
|
||||
shadow-pixel-sm
|
||||
p-1
|
||||
mb-8
|
||||
">
|
||||
<TabsTrigger
|
||||
value="owned"
|
||||
className="
|
||||
font-sans
|
||||
font-semibold
|
||||
data-[state=active]:bg-pixel-cyan-bright
|
||||
data-[state=active]:text-white
|
||||
data-[state=active]:shadow-pixel-sm
|
||||
transition-all
|
||||
duration-100
|
||||
"
|
||||
>
|
||||
My Documents ({ownedDocuments.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="shared"
|
||||
className="
|
||||
font-sans
|
||||
font-semibold
|
||||
data-[state=active]:bg-pixel-cyan-bright
|
||||
data-[state=active]:text-white
|
||||
data-[state=active]:shadow-pixel-sm
|
||||
transition-all
|
||||
duration-100
|
||||
"
|
||||
>
|
||||
Shared with Me ({sharedDocuments.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Owned Documents Tab */}
|
||||
<TabsContent value="owned">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{ownedDocuments.length === 0 ? (
|
||||
<p className="col-span-full text-center text-pixel-text-muted font-sans py-12">
|
||||
No documents yet. Create one to get started!
|
||||
</p>
|
||||
</div>
|
||||
<div className="doc-actions">
|
||||
<button onClick={() => navigate(`/${doc.type}/${doc.id}`)} aria-label={`Open ${doc.name}`}>
|
||||
<PixelIcon name="back-arrow" size={16} style={{ transform: 'rotate(180deg)' }} />
|
||||
<span style={{ marginLeft: '6px' }}>Open</span>
|
||||
</button>
|
||||
<button onClick={() => deleteDocument(doc.id)} aria-label={`Delete ${doc.name}`}>
|
||||
<PixelIcon name="trash" size={16} />
|
||||
<span style={{ marginLeft: '6px' }}>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
ownedDocuments.map((doc) => (
|
||||
<DocumentCard
|
||||
key={doc.id}
|
||||
doc={doc}
|
||||
onDelete={deleteDocument}
|
||||
isShared={false}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Shared Documents Tab */}
|
||||
<TabsContent value="shared">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{sharedDocuments.length === 0 ? (
|
||||
<p className="col-span-full text-center text-pixel-text-muted font-sans py-12">
|
||||
No shared documents yet.
|
||||
</p>
|
||||
) : (
|
||||
sharedDocuments.map((doc) => (
|
||||
<DocumentCard
|
||||
key={doc.id}
|
||||
doc={doc}
|
||||
onDelete={deleteDocument}
|
||||
isShared={true}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user