Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 8 additions & 17 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { listProjects } from "@/lib/projects";
import { templates } from "@/lib/templates";
import { NewProjectButton } from "@/components/dashboard/NewProjectButton";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { TemplateGrid } from "@/components/dashboard/TemplateGrid";
import { ProjectsGallery } from "@/components/dashboard/ProjectsGallery";

Expand All @@ -18,26 +18,17 @@ export default function DashboardPage() {
}));

return (
<div className="min-h-screen">
<header className="sticky top-0 z-10 glass border-b border-border">
<div className="mx-auto flex h-14 max-w-5xl items-center px-6">
<span className="text-base font-semibold tracking-tight">TexSet</span>
</div>
</header>
<div className="flex min-h-screen">
<Sidebar />

<main className="mx-auto max-w-5xl animate-fade-in space-y-10 px-6 py-10">
<section className="space-y-3">
<h2 className="text-sm font-medium text-text-muted">New document</h2>
<NewProjectButton />
</section>

<section className="space-y-3">
<h2 className="text-sm font-medium text-text-muted">Templates</h2>
<main className="min-w-0 flex-1 animate-fade-in space-y-10 px-8 py-10">
<section id="templates" className="space-y-4">
<h2 className="text-lg font-semibold">Templates</h2>
<TemplateGrid templates={templateCards} />
</section>

<section className="space-y-3">
<h2 className="text-sm font-medium text-text-muted">My projects</h2>
<section id="projects" className="space-y-4">
<h2 className="text-lg font-semibold">My Projects</h2>
<ProjectsGallery initialProjects={projects} />
</section>
</main>
Expand Down
3 changes: 2 additions & 1 deletion src/components/dashboard/NewProjectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export function NewProjectButton() {
icon={Plus}
loading={pending === "blank"}
onClick={() => create({}, "blank")}
className="w-full"
>
Blank document
New document
</Button>
);
}
34 changes: 17 additions & 17 deletions src/components/dashboard/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,60 +19,60 @@ export function ProjectCard({
onTogglePin,
onDelete,
}: ProjectCardProps) {
// data-engine sets the accent (green for LaTeX, blue for Typst) for everything
// tinted in this card: the top stripe, the badge, and the pinned ring
return (
<div className="group relative">
<div className="group relative" data-engine={project.engine}>
<Link
href={`/editor/${project.id}`}
className="block overflow-hidden rounded-xl border border-border bg-surface shadow-soft transition hover:-translate-y-0.5 hover:shadow-lift"
className={clsx(
"block overflow-hidden rounded-xl border bg-surface shadow-soft transition hover:-translate-y-0.5 hover:shadow-lift",
project.pinned ? "border-accent ring-1 ring-accent" : "border-border",
)}
>
<div className="h-1 bg-accent" />
<ProjectThumbnail projectId={project.id} />

<div className="space-y-1 p-3">
<div className="flex items-center gap-1.5">
{project.pinned && (
<Pin className="h-3 w-3 shrink-0 fill-accent text-accent" />
)}
<span className="truncate font-medium">{project.name}</span>
</div>
<span className="block truncate font-medium">{project.name}</span>
<p className="text-xs text-text-muted" suppressHydrationWarning>
Edited {formatRelativeTime(project.updatedAt)}
</p>
<div className="flex items-center justify-between pt-0.5">
<span className="text-xs text-text-muted/70">
Created {formatDate(project.createdAt)}
</span>
<span
data-engine={project.engine}
className="rounded bg-accent/12 px-1.5 py-0.5 text-[10px] font-medium text-accent"
>
<span className="rounded-md bg-accent/15 px-2 py-0.5 text-[11px] font-semibold text-accent">
{engines[project.engine].name}
</span>
</div>
</div>
</Link>

{/* actions sit over the card; preventDefault keeps the link from firing */}
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition group-hover:opacity-100">
<div className="absolute right-2 top-3 flex gap-1">
<button
onClick={(event) => {
event.preventDefault();
onTogglePin(project);
}}
className={clsx(
"flex h-7 w-7 items-center justify-center rounded-md bg-surface/90 shadow-soft backdrop-blur transition hover:bg-surface",
project.pinned ? "text-accent" : "text-text-muted hover:text-text",
"flex h-7 w-7 items-center justify-center rounded-md shadow-soft backdrop-blur transition",
project.pinned
? "bg-accent text-accent-fg"
: "bg-surface/90 text-text-muted opacity-0 hover:text-text group-hover:opacity-100",
)}
aria-label={project.pinned ? "Unpin" : "Pin to top"}
title={project.pinned ? "Unpin" : "Pin to top"}
>
<Pin className={clsx("h-3.5 w-3.5", project.pinned && "fill-accent")} />
<Pin className={clsx("h-3.5 w-3.5", project.pinned && "fill-current")} />
</button>
<button
onClick={(event) => {
event.preventDefault();
onDelete(project);
}}
className="flex h-7 w-7 items-center justify-center rounded-md bg-surface/90 text-text-muted shadow-soft backdrop-blur transition hover:bg-surface hover:text-danger"
className="flex h-7 w-7 items-center justify-center rounded-md bg-surface/90 text-text-muted opacity-0 shadow-soft backdrop-blur transition hover:text-danger group-hover:opacity-100"
aria-label="Delete"
title="Delete"
>
Expand Down
76 changes: 46 additions & 30 deletions src/components/dashboard/ProjectsGallery.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import { Pin } from "lucide-react";
import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
import type { Project } from "@/lib/projects";
import { ProjectCard } from "./ProjectCard";
Expand All @@ -11,9 +12,7 @@ const PAGE_SIZE = 8;
function sortProjects(list: Project[]): Project[] {
return [...list].sort((a, b) => {
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
return (
(b.lastOpenedAt ?? b.updatedAt) - (a.lastOpenedAt ?? a.updatedAt)
);
return (b.lastOpenedAt ?? b.updatedAt) - (a.lastOpenedAt ?? a.updatedAt);
});
}

Expand All @@ -26,18 +25,15 @@ export function ProjectsGallery({
const [showAll, setShowAll] = useState(false);
const [pendingDelete, setPendingDelete] = useState<Project | null>(null);

// when the server sends a fresh list (e.g. after navigating back from a
// project), adopt it so deleted or newly created projects are reflected
// adopt a fresh server list (e.g. after navigating back from a project)
useEffect(() => {
setProjects(initialProjects);
}, [initialProjects]);

async function togglePin(project: Project) {
const pinned = !project.pinned;
setProjects((prev) =>
sortProjects(
prev.map((p) => (p.id === project.id ? { ...p, pinned } : p)),
),
sortProjects(prev.map((p) => (p.id === project.id ? { ...p, pinned } : p))),
);
await fetch(`/api/projects/${project.id}`, {
method: "PATCH",
Expand All @@ -58,36 +54,56 @@ export function ProjectsGallery({
return (
<div className="rounded-xl border border-dashed border-border bg-surface/50 p-10 text-center">
<p className="text-text-muted">
No documents yet. Start with a blank document or pick a template above.
No documents yet. Create one with New document or pick a template above.
</p>
</div>
);
}

const visible = showAll ? projects : projects.slice(0, PAGE_SIZE);
const pinned = projects.filter((p) => p.pinned);
const rest = projects.filter((p) => !p.pinned);
const visibleRest = showAll ? rest : rest.slice(0, PAGE_SIZE);

return (
<>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{visible.map((project) => (
<ProjectCard
key={project.id}
project={project}
onTogglePin={togglePin}
onDelete={setPendingDelete}
/>
))}
</div>
const card = (project: Project) => (
<ProjectCard
key={project.id}
project={project}
onTogglePin={togglePin}
onDelete={setPendingDelete}
/>
);

{projects.length > PAGE_SIZE && (
<button
onClick={() => setShowAll((open) => !open)}
className="mt-4 text-sm font-medium text-accent transition hover:brightness-110"
>
{showAll ? "Show less" : `Show all ${projects.length}`}
</button>
return (
<div className="space-y-6">
{pinned.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-1.5 text-sm font-medium text-text-muted">
<Pin className="h-3.5 w-3.5 fill-current" />
Pinned
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{pinned.map(card)}
</div>
</div>
)}

<div className="space-y-3">
{pinned.length > 0 && rest.length > 0 && (
<div className="text-sm font-medium text-text-muted">Recent</div>
)}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{visibleRest.map(card)}
</div>
{rest.length > PAGE_SIZE && (
<button
onClick={() => setShowAll((open) => !open)}
className="text-sm font-medium text-accent transition hover:brightness-110"
>
{showAll ? "Show less" : `Show all ${rest.length}`}
</button>
)}
</div>

{pendingDelete && (
<ConfirmDialog
title="Delete document"
Expand All @@ -97,6 +113,6 @@ export function ProjectsGallery({
onCancel={() => setPendingDelete(null)}
/>
)}
</>
</div>
);
}
31 changes: 31 additions & 0 deletions src/components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NewProjectButton } from "./NewProjectButton";

// Left navigation for the dashboard. Holds the brand, the primary action, and
// jumps to the page sections. Roomy enough to grow an engine filter and a theme
// toggle later.
export function Sidebar() {
return (
<aside className="sticky top-0 flex h-screen w-56 shrink-0 flex-col gap-6 border-r border-border bg-surface px-4 py-5">
<span className="px-1 text-xl font-bold tracking-tight">
Tex<span className="text-accent">Set</span>
</span>

<NewProjectButton />

<nav className="flex flex-col gap-0.5 text-sm">
<a
href="#templates"
className="rounded-md px-2 py-1.5 text-text-muted transition hover:bg-surface-2 hover:text-text"
>
Templates
</a>
<a
href="#projects"
className="rounded-md px-2 py-1.5 text-text-muted transition hover:bg-surface-2 hover:text-text"
>
My Projects
</a>
</nav>
</aside>
);
}
5 changes: 3 additions & 2 deletions src/components/dashboard/TemplateGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ export function TemplateGrid({ templates }: { templates: TemplateCard[] }) {
data-engine={template.engine}
onClick={() => create({ templateId: template.id }, template.id)}
disabled={isPending}
className="group flex flex-col items-start gap-3 rounded-xl border border-border bg-surface p-4 text-left shadow-soft transition hover:-translate-y-0.5 hover:shadow-lift disabled:opacity-60"
className="group relative flex flex-col items-start gap-3 overflow-hidden rounded-xl border border-border bg-surface p-4 pt-5 text-left shadow-soft transition hover:-translate-y-0.5 hover:shadow-lift disabled:opacity-60"
>
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/12 text-accent">
<span className="absolute inset-x-0 top-0 h-1 bg-accent" />
<span className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/15 text-accent">
{isPending ? (
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
Expand Down
Loading