public
anord
read
Ledger
Why work hard when you can work easier?
Languages
Repository composition by tracked source files.
TypeScript
86%
CSS
10%
SQL
3%
Shell
1%
HTML
0%
Trace
apps/web/src/components/SpacesPage.tsx
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
import { useMemo, useState } from "react";
2
import { Link } from "react-router-dom";
3
import type { PageSummary } from "@ledger/shared";
4
import { EmptyState } from "./EmptyState";
5
import { Icon } from "./Icon";
6
import { PageHeader } from "./PageHeader";
8
type Space = {
9
id: string;
10
name: string;
11
key: string;
12
visibility: string;
13
};
15
export function SpacesPage({
16
spaces,
17
pagesBySpace
18
}: {
19
spaces: Space[];
20
pagesBySpace: Record<string, PageSummary[]>;
21
}) {
22
const [tab, setTab] = useState<"updated" | "public" | "internal">("updated");
23
const collections = useMemo(
24
() =>
25
spaces
26
.map((space) => ({
27
...space,
28
count: (pagesBySpace[space.key] ?? []).length
29
}))
30
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
31
[pagesBySpace, spaces]
32
);
33
const allPages = useMemo(
34
() =>
35
Object.values(pagesBySpace)
36
.flat()
37
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)),
38
[pagesBySpace]
39
);
41
const visiblePages = allPages.filter((page) => {
42
if (tab === "public") {
43
return page.visibility === "public";
44
}
46
if (tab === "internal") {
47
return page.visibility !== "public";
48
}
50
return true;
51
});
53
return (
54
<div className="home-page">
55
<PageHeader
56
eyebrow="Knowledge base"
57
title="Home"
58
description="Open the latest docs, move between collections quickly, and keep the knowledge base feeling lightweight instead of buried in admin screens."
59
/>
61
{allPages.length === 0 ? (
62
<section className="panel panel-muted">
63
<EmptyState
64
title="No documents published yet"
65
description="Create your first pages and they will appear here as the workspace home feed."
66
/>
67
</section>
68
) : (
69
<>
70
<div className="home-tabs" role="tablist" aria-label="Home filters">
71
<button type="button" className={`home-tab${tab === "updated" ? " is-active" : ""}`} onClick={() => setTab("updated")}>
72
Recently updated
73
</button>
74
<button type="button" className={`home-tab${tab === "public" ? " is-active" : ""}`} onClick={() => setTab("public")}>
75
Public docs
76
</button>
77
<button type="button" className={`home-tab${tab === "internal" ? " is-active" : ""}`} onClick={() => setTab("internal")}>
78
Internal docs
79
</button>
80
</div>
82
<div className="home-content-grid">
83
<section className="document-feed">
84
{visiblePages.map((page) => {
85
const space = spaces.find((entry) => entry.id === page.spaceId);
86
return (
87
<Link key={page.id} to={`/page/${page.slug}`} className="document-feed__item">
88
<div className="document-feed__icon">
89
<Icon name="document" className="icon icon-sm" />
90
</div>
91
<div className="document-feed__body">
92
<strong className="document-feed__title">{page.title}</strong>
93
<p className="document-feed__meta">
94
Updated {new Date(page.updatedAt).toLocaleDateString()} in {space?.name ?? "Collection"} - {page.visibility}
95
</p>
96
</div>
97
</Link>
98
);
99
})}
100
</section>
102
<aside className="home-side-column">
103
<div className="section-head section-head--compact">
104
<div>
105
<p className="eyebrow">Quick access</p>
106
<h3>Collections</h3>
107
</div>
108
</div>
109
<div className="collection-list">
110
{collections.map((space) => (
111
<Link key={space.id} to={`/space/${space.key}`} className="collection-list__item">
112
<div className="collection-list__body">
113
<strong>{space.name}</strong>
114
<p>{space.count} documents</p>
115
</div>
116
<span className={`badge badge-${space.visibility}`}>{space.visibility}</span>
117
</Link>
118
))}
119
</div>
120
</aside>
121
</div>
122
</>
123
)}
124
</div>
125
);
126
}