Skip to content

Commit 0e1e44c

Browse files
authored
WEB-103 Refactor DocumentList and MediaLibrary to reuse components (#105)
2 parents 1a0c3d8 + b161b31 commit 0e1e44c

3 files changed

Lines changed: 125 additions & 78 deletions

File tree

src/app/editor/DocumentList.tsx

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import Link from "next/link";
44
import { useRouter } from "next/navigation";
55
import { useTransition } from "react";
6+
import { FileText } from "lucide-react";
67
import { createDocumentAction } from "../../lib/actions";
78
import { getDocumentName } from "../../lib/documents";
9+
import { ResourceCard, NewResourceCard, formatRelativeTime } from "./ResourceCard";
810

911
function NewDocumentCard() {
1012
const router = useRouter();
@@ -38,51 +40,25 @@ function NewDocumentCard() {
3840
}
3941

4042
return (
41-
<button
42-
type="button"
43-
onClick={handleCreateDocument}
43+
<NewResourceCard
44+
label="New Document"
45+
loadingLabel="Creating..."
4446
disabled={isCreating}
45-
className="flex h-48 w-32 shrink-0 cursor-pointer rounded-lg flex-col items-center justify-center border border-dashed border-gray-400 bg-white p-4 text-center text-gray-700 transition-colors hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-60"
46-
>
47-
<span className="text-3xl leading-none">+</span>
48-
<span className="mt-2 text-sm font-medium">
49-
{isCreating ? "Creating..." : "New Document"}
50-
</span>
51-
</button>
47+
onClick={handleCreateDocument}
48+
/>
5249
);
5350
}
5451

55-
function formatRelativeTime(date: Date): string {
56-
const now = Date.now();
57-
const seconds = Math.floor((now - date.getTime()) / 1000);
58-
59-
if (seconds < 60) return "just now";
60-
61-
const minutes = Math.floor(seconds / 60);
62-
if (minutes < 60) return `${minutes} min ago`;
63-
64-
const hours = Math.floor(minutes / 60);
65-
if (hours < 24) return `${hours} hr ago`;
66-
67-
const days = Math.floor(hours / 24);
68-
if (days < 7) return `${days} ${days === 1 ? "day" : "days"} ago`;
69-
const weeks = Math.floor(days / 7);
70-
if (days < 30) return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`;
71-
const months = Math.floor(days / 30);
72-
if (days < 365) return `${months} ${months === 1 ? "mo" : "mos"} ago`;
73-
const years = Math.floor(days / 365);
74-
return `${years} ${years === 1 ? "yr" : "yrs"} ago`;
75-
}
76-
7752
function DocumentCard({ id, name, lastModified }: { id: number; name: string | null; lastModified: Date | null }) {
53+
const displayName = getDocumentName({ id, name });
54+
7855
return (
79-
<Link href={`/editor/${id}`} className="flex flex-col bg-gray-100 w-32 h-48 shrink-0 rounded-lg p-2">
80-
<div className="flex flex-1 w-full items-center justify-center text-center font-medium line-clamp-3 break-words">
81-
{getDocumentName({ id, name })}
82-
</div>
83-
<div className="text-xs text-gray-600 px-2 py-1 text-center">
84-
{lastModified ? formatRelativeTime(lastModified) : "No versions"}
85-
</div>
56+
<Link href={`/editor/${id}`} className="block">
57+
<ResourceCard
58+
preview={<FileText className="h-10 w-10 text-gray-400" />}
59+
name={displayName}
60+
date={lastModified ? formatRelativeTime(lastModified) : "No versions"}
61+
/>
8662
</Link>
8763
);
8864
}

src/app/editor/MediaLibrary.tsx

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { File as FileIcon, Trash2, Copy } from "lucide-react";
55
import { uploadMediaAction, deleteMediaAction } from "../../lib/media-actions";
66
import type { MediaFile } from "../../lib/types";
77
import { runAction } from "./runAction";
8+
import { ResourceCard, NewResourceCard, formatRelativeTime } from "./ResourceCard";
89

910
function formatFileSize(bytes: number): string {
1011
if (bytes < 1024) return `${bytes} B`;
@@ -21,31 +22,25 @@ function UploadCard({
2122
}) {
2223
const inputRef = useRef<HTMLInputElement>(null);
2324

24-
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
25-
const file = e.target.files?.[0];
26-
if (file) onFileSelected(file);
27-
e.target.value = "";
28-
}
29-
3025
return (
31-
<button
32-
type="button"
33-
onClick={() => inputRef.current?.click()}
26+
<NewResourceCard
27+
label="Upload File"
28+
loadingLabel="Uploading..."
3429
disabled={disabled}
35-
className="flex h-48 w-32 shrink-0 cursor-pointer rounded-lg flex-col items-center justify-center border border-dashed border-gray-400 bg-white p-4 text-center text-gray-700 transition-colors hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-60"
30+
onClick={() => inputRef.current?.click()}
3631
>
37-
<span className="text-3xl leading-none">+</span>
38-
<span className="mt-2 text-sm font-medium">
39-
{disabled ? "Uploading..." : "Upload File"}
40-
</span>
4132
<input
4233
ref={inputRef}
4334
type="file"
4435
hidden
45-
onChange={handleChange}
36+
onChange={(e) => {
37+
const file = e.target.files?.[0];
38+
if (file) onFileSelected(file);
39+
e.target.value = "";
40+
}}
4641
disabled={disabled}
4742
/>
48-
</button>
43+
</NewResourceCard>
4944
);
5045
}
5146

@@ -59,37 +54,28 @@ function MediaCard({
5954
disabled: boolean;
6055
}) {
6156
const isImage = file.contentType?.startsWith("image/");
62-
63-
function handleCopyUrl() {
64-
navigator.clipboard.writeText(file.url);
65-
}
57+
const createdDate = file.createdAt ? formatRelativeTime(new Date(file.createdAt)) : null;
6658

6759
return (
68-
<div className="flex h-48 w-32 shrink-0 flex-col rounded-lg bg-gray-100 overflow-hidden">
69-
<div className="flex h-28 items-center justify-center bg-gray-200">
70-
{isImage ? (
60+
<ResourceCard
61+
preview={
62+
isImage ? (
7163
<img
7264
src={file.url}
7365
alt={file.name}
7466
className="h-full w-full object-cover"
7567
/>
7668
) : (
7769
<FileIcon className="h-10 w-10 text-gray-400" />
78-
)}
79-
</div>
80-
81-
<div className="flex flex-1 flex-col p-2">
82-
<span className="truncate text-xs font-medium" title={file.name}>
83-
{file.name}
84-
</span>
85-
<span className="text-xs text-gray-500">
86-
{formatFileSize(file.size)}
87-
</span>
88-
89-
<div className="mt-auto flex gap-1">
70+
)
71+
}
72+
name={file.name}
73+
date={createdDate ? `${createdDate} \u00B7 ${formatFileSize(file.size)}` : formatFileSize(file.size)}
74+
actions={
75+
<>
9076
<button
9177
type="button"
92-
onClick={handleCopyUrl}
78+
onClick={() => navigator.clipboard.writeText(file.url)}
9379
className="rounded p-1 text-gray-500 hover:text-blue-600"
9480
title="Copy URL"
9581
>
@@ -104,9 +90,9 @@ function MediaCard({
10490
>
10591
<Trash2 className="h-3 w-3" />
10692
</button>
107-
</div>
108-
</div>
109-
</div>
93+
</>
94+
}
95+
/>
11096
);
11197
}
11298

src/app/editor/ResourceCard.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { ReactNode } from "react";
2+
3+
export function formatRelativeTime(date: Date): string {
4+
const now = Date.now();
5+
const seconds = Math.floor((now - date.getTime()) / 1000);
6+
7+
if (seconds < 60) return "just now";
8+
9+
const minutes = Math.floor(seconds / 60);
10+
if (minutes < 60) return `${minutes} min ago`;
11+
12+
const hours = Math.floor(minutes / 60);
13+
if (hours < 24) return `${hours} hr ago`;
14+
15+
const days = Math.floor(hours / 24);
16+
if (days < 7) return `${days} ${days === 1 ? "day" : "days"} ago`;
17+
const weeks = Math.floor(days / 7);
18+
if (days < 30) return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`;
19+
const months = Math.floor(days / 30);
20+
if (days < 365) return `${months} ${months === 1 ? "mo" : "mos"} ago`;
21+
const years = Math.floor(days / 365);
22+
return `${years} ${years === 1 ? "yr" : "yrs"} ago`;
23+
}
24+
25+
export function ResourceCard({
26+
preview,
27+
name,
28+
date,
29+
actions,
30+
}: {
31+
preview: ReactNode;
32+
name: string;
33+
date: string | null;
34+
actions?: ReactNode;
35+
}) {
36+
return (
37+
<div className="flex h-48 w-32 shrink-0 flex-col rounded-lg bg-gray-100 overflow-hidden">
38+
<div className="flex h-24 items-center justify-center bg-gray-200">
39+
{preview}
40+
</div>
41+
42+
<div className="flex flex-1 flex-col p-2">
43+
<span className="truncate text-xs font-medium" title={name}>
44+
{name}
45+
</span>
46+
<span className="text-xs text-gray-500">
47+
{date ?? "\u00A0"}
48+
</span>
49+
50+
{actions && (
51+
<div className="mt-auto flex gap-1">{actions}</div>
52+
)}
53+
</div>
54+
</div>
55+
);
56+
}
57+
58+
export function NewResourceCard({
59+
label,
60+
loadingLabel,
61+
disabled,
62+
onClick,
63+
children,
64+
}: {
65+
label: string;
66+
loadingLabel: string;
67+
disabled: boolean;
68+
onClick: () => void;
69+
children?: ReactNode;
70+
}) {
71+
return (
72+
<button
73+
type="button"
74+
onClick={onClick}
75+
disabled={disabled}
76+
className="flex h-48 w-32 shrink-0 cursor-pointer rounded-lg flex-col items-center justify-center border border-dashed border-gray-400 bg-white p-4 text-center text-gray-700 transition-colors hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-60"
77+
>
78+
<span className="text-3xl leading-none">+</span>
79+
<span className="mt-2 text-sm font-medium">
80+
{disabled ? loadingLabel : label}
81+
</span>
82+
{children}
83+
</button>
84+
);
85+
}

0 commit comments

Comments
 (0)