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
1 change: 0 additions & 1 deletion apps/blog-platform/app/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import type { ComponentType } from "react";

import Link from "next/link";
import Image from "next/image";
Expand Down
19 changes: 18 additions & 1 deletion apps/server/src/middleware/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ function getClientKey(req: Request): string {
return ip || "unknown";
}

function getSanitizedPath(path: string): string {
return path
.split("/")
.map((segment) => {
if (
/^\d+$/.test(segment) ||
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment) ||
(segment.length >= 10 && /[a-zA-Z]/.test(segment) && /[0-9]/.test(segment))
) {
return ":id";
}
return segment;
})
.join("/");
}

function getRouteLimitConfig(req: Request) {
const isAuthRoute = req.path.startsWith("/api/v1/auth");

Expand Down Expand Up @@ -54,7 +70,8 @@ export const rateLimitMiddleware = (req: Request, res: Response, next: NextFunct
const { windowMs, maxRequests } = getRouteLimitConfig(req);
const key = getClientKey(req);

const redisKey = `rate-limit:${req.method}:${req.path}:${key}`;
const sanitizedPath = getSanitizedPath(req.path);
const redisKey = `rate-limit:${req.method}:${sanitizedPath}:${key}`;

const checkWithFallback = async () => {
try {
Expand Down
39 changes: 25 additions & 14 deletions apps/studio/app/(main)/(dashboard)/components/OverviewHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { BookOpen, FolderOpen, ArrowRight, BriefcaseBusiness } from "lucide-reac
import { Card } from "@veriworkly/ui";

import {
getResumeWorkspaceSnapshot,
subscribeToResumeWorkspace,
RESUME_WORKSPACE_SERVER_SNAPSHOT,
} from "@/features/documents/services/resume-workspace";
getDocumentLibrarySnapshot,
subscribeToDocumentLibrary,
DOCUMENT_LIBRARY_SERVER_SNAPSHOT,
} from "@/features/documents/services/document-library";

import RecentCard from "./RecentCard";
import OverviewHomeHeader from "./OverviewHomeHeader";
Expand All @@ -33,18 +33,27 @@ function MiniLink({ href, icon: Icon, label }: { href: string; icon: LucideIcon;

const OverviewHome = () => {
const snapshot = useSyncExternalStore(
subscribeToResumeWorkspace,
() => getResumeWorkspaceSnapshot(),
() => RESUME_WORKSPACE_SERVER_SNAPSHOT,
subscribeToDocumentLibrary,
() => getDocumentLibrarySnapshot(),
() => DOCUMENT_LIBRARY_SERVER_SNAPSHOT,
);

const totalCount = snapshot.counts.RESUME;
const totalCount =
snapshot.counts.RESUME +
snapshot.counts.COVER_LETTER +
snapshot.counts.FORMAL_LETTER +
snapshot.counts.INVOICE;
const resumeCount = snapshot.counts.RESUME;
const coverLetterCount = snapshot.counts.COVER_LETTER;
const recentDocs = snapshot.docs.slice(0, 6);

return (
<section className="space-y-7" aria-label="Studio overview">
<OverviewHomeHeader totalCount={totalCount} resumeCount={resumeCount} />
<OverviewHomeHeader
totalCount={totalCount}
resumeCount={resumeCount}
coverLetterCount={coverLetterCount}
/>

<OverviewReferenceCard />

Expand All @@ -54,11 +63,11 @@ const OverviewHome = () => {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-black">Recently opened</h2>
<p className="text-muted text-sm">Compact view, not another full resume page.</p>
<p className="text-muted text-sm">Compact view of your recent documents.</p>
</div>

<Link href="/documents" className="text-accent text-sm font-bold">
All resumes
All documents
</Link>
</div>

Expand All @@ -70,7 +79,7 @@ const OverviewHome = () => {
<p className="font-bold">No files yet</p>

<p className="text-muted mt-1 text-sm">
Use New Document in sidebar to create first resume.
Use New Document in sidebar to create your first file.
</p>
</div>
)}
Expand All @@ -80,12 +89,14 @@ const OverviewHome = () => {
<aside className="space-y-4 p-5 sm:p-6">
<div>
<h2 className="text-sm font-black">Workspace shortcuts</h2>
<p className="text-muted mt-1 text-xs">Profile data, roadmap, and resume library.</p>
<p className="text-muted mt-1 text-xs">
Profile data, roadmap, and document library.
</p>
</div>

<div className="grid gap-2">
<MiniLink href="/profile" icon={BriefcaseBusiness} label="Profile workspace" />
<MiniLink href="/documents" icon={FolderOpen} label="Resume library" />
<MiniLink href="/documents" icon={FolderOpen} label="Document library" />
<MiniLink href="/admin/roadmap" icon={BookOpen} label="Roadmap" />
</div>
</aside>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ function Stat({ label, value }: { label: string; value: number }) {

const OverviewHomeHeader = ({
totalCount,
coverLetterCount,
resumeCount,
}: {
totalCount: number;
coverLetterCount: number;
resumeCount: number;
}) => {
const user = useUserStore((state) => state.user);
Expand All @@ -38,15 +40,15 @@ const OverviewHomeHeader = ({
</h1>

<p className="text-muted mt-2 max-w-2xl text-base">
Recent resumes, useful references, and account shortcuts without duplicating the full
Recent documents, useful references, and account shortcuts without duplicating the full
library page.
</p>
</div>

<div className="grid grid-cols-3 gap-2 text-center">
<Stat label="Files" value={totalCount} />
<Stat label="Resumes" value={resumeCount} />
<Stat label="Shared" value={0} />
<Stat label="Letters" value={coverLetterCount} />
</div>
</div>
</header>
Expand Down
15 changes: 11 additions & 4 deletions apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ import { FileText } from "lucide-react";
import { Badge } from "@veriworkly/ui";

import { getDocumentDefinition } from "@/features/documents/core/registry";
import { type ResumeWorkspaceDoc } from "@/features/documents/services/resume-workspace";
import { type DocumentLibraryItem } from "@/features/documents/services/document-library";

const RecentCard = ({ doc }: { doc: ResumeWorkspaceDoc }) => {
const RecentCard = ({ doc }: { doc: DocumentLibraryItem }) => {
const definition = getDocumentDefinition(doc.type);

return (
<Link
href={`/editor/${doc.type.toLowerCase()}/${doc.id}`}
className="border-border bg-background/70 group hover:border-accent/40 hover:bg-card overflow-hidden rounded-xl border transition"
>
<div className="border-border/70 relative h-28 border-b bg-[color-mix(in_oklab,var(--card)_78%,var(--background))]">
<div className="border-border/70 relative h-32 border-b bg-[color-mix(in_oklab,var(--card)_78%,var(--background))]">
{doc.previewImage ? (
<Image src={doc.previewImage} alt="" fill className="object-contain p-2" sizes="20vw" />
<Image
fill
alt=""
priority
sizes="80vw"
src={doc.previewImage}
className="object-cover object-top p-2"
/>
) : (
<div className="flex h-full items-center justify-center">
<FileText className="text-accent h-8 w-8" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import { Copy, Cloud, Share2, Trash2, RefreshCw, ExternalLink, MoreHorizontal } from "lucide-react";
import { toast } from "sonner";

import { Menu, MenuItem, MenuSeparator } from "@veriworkly/ui";

import type { DocumentLibraryItem } from "@/features/documents/services/document-library";

export interface DocumentActionsMenuProps {
doc: DocumentLibraryItem;
syncing: boolean;
onDeleteAction: (doc: DocumentLibraryItem) => void;
onShareAction: (doc: DocumentLibraryItem) => void;
onSyncNowAction: (id: string) => void;
onSyncDetailsAction: (id: string) => void;
}

export function DocumentActionsMenu({
doc,
syncing,
onDeleteAction,
onShareAction,
onSyncNowAction,
onSyncDetailsAction,
}: DocumentActionsMenuProps) {
return (
<Menu
size="sm"
panelClassName="z-50"
trigger={({ open, toggle, menuId }) => (
<button
type="button"
aria-expanded={open}
aria-haspopup="menu"
aria-controls={open ? menuId : undefined}
aria-label={`Open actions for ${doc.title}`}
className="border-border bg-card/90 text-foreground hover:bg-background flex h-9 w-9 items-center justify-center rounded-xl border shadow-sm backdrop-blur"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
toggle();
}}
>
<MoreHorizontal className="h-4 w-4" />
</button>
)}
>
{({ close }) => (
<>
<MenuItem
className="h-8 rounded-lg text-xs"
onClick={() => {
close();
window.location.href = `/editor/${doc.type.toLowerCase()}/${doc.id}`;
}}
>
<ExternalLink className="h-4 w-4" />
Open
</MenuItem>

<MenuItem
className="h-8 rounded-lg text-xs"
onClick={() => {
close();
onShareAction(doc);
}}
>
<Share2 className="h-4 w-4" />
Create public link
</MenuItem>

<MenuItem
className="h-8 rounded-lg text-xs"
disabled={syncing}
onClick={() => {
close();
onSyncNowAction(doc.id);
}}
>
<RefreshCw className="h-4 w-4" />
{syncing ? "Syncing..." : "Sync now"}
</MenuItem>

<MenuItem
className="h-8 rounded-lg text-xs"
onClick={() => {
close();
onSyncDetailsAction(doc.id);
}}
>
<Cloud className="h-4 w-4" />
View sync details
</MenuItem>

<MenuItem
className="h-8 rounded-lg text-xs"
onClick={() => {
close();
void navigator.clipboard.writeText(
`${window.location.origin}/editor/${doc.type.toLowerCase()}/${doc.id}`,
);
toast.success("Document link copied");
}}
>
<Copy className="h-4 w-4" />
Copy link
</MenuItem>

<MenuSeparator />

<MenuItem
className="text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10 h-8 rounded-lg text-xs"
onClick={() => {
close();
onDeleteAction(doc);
}}
>
<Trash2 className="h-4 w-4" />
Delete
</MenuItem>
</>
)}
</Menu>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import Link from "next/link";

import { Badge, Button } from "@veriworkly/ui";

import type { ResumeSyncTelemetry } from "@/features/resume/services/resume-sync";
import type { DocumentLibraryItem } from "@/features/documents/services/document-library";

import { getDocumentDefinition } from "@/features/documents/core/registry";
import { formatRelative } from "@/features/documents/services/document-library";

import { DocumentActionsMenu } from "./DocumentActionsMenu";
import { docIconMap, getSyncLabel, getActivityLabel } from "./document-display";

interface DocumentListRowProps {
doc: DocumentLibraryItem;
syncing: boolean;
telemetry: ResumeSyncTelemetry | null;
onDeleteAction: (doc: DocumentLibraryItem) => void;
onShareAction: (doc: DocumentLibraryItem) => void;
onSyncNowAction: (id: string) => void;
onSyncDetailsAction: (id: string) => void;
}

export function DocumentListRow({
doc,
syncing,
telemetry,
onDeleteAction,
onShareAction,
onSyncNowAction,
onSyncDetailsAction,
}: DocumentListRowProps) {
const Icon = docIconMap[doc.type];

return (
<article className="grid gap-3 p-4 sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center sm:p-5">
<div className="border-border bg-background flex h-12 w-10 shrink-0 items-center justify-center rounded-lg border sm:h-14 sm:w-11">
<Icon className="text-accent h-5 w-5" />
</div>

<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h2 className="truncate text-sm font-bold">{doc.title}</h2>
<Badge className="px-2 py-0.5 text-[10px]">{getDocumentDefinition(doc.type).label}</Badge>
</div>

<p className="text-muted mt-1 truncate text-xs">{doc.description}</p>

<div className="mt-2 flex flex-wrap gap-2 text-[11px]">
<span className="bg-muted/25 rounded-md px-2 py-1">{doc.templateName}</span>

<span className="bg-muted/25 rounded-md px-2 py-1">{getSyncLabel(doc.sync)}</span>

<span className="bg-muted/25 rounded-md px-2 py-1">
{getActivityLabel(doc.sync, telemetry)}
</span>

<span className="text-muted px-1 py-1" suppressHydrationWarning>
Updated {formatRelative(doc.updatedAt)}
</span>
</div>
</div>

<div className="flex items-center gap-2 sm:justify-end">
<Button asChild size="sm" variant="secondary">
<Link href={`/editor/${doc.type.toLowerCase()}/${doc.id}`}>Open</Link>
</Button>

<DocumentActionsMenu
doc={doc}
syncing={syncing}
onShareAction={onShareAction}
onDeleteAction={onDeleteAction}
onSyncNowAction={onSyncNowAction}
onSyncDetailsAction={onSyncDetailsAction}
/>
</div>
</article>
);
}
Loading
Loading