Skip to content

Commit 703fd2c

Browse files
committed
view: add cards view
1 parent c909e43 commit 703fd2c

File tree

5 files changed

+128
-0
lines changed

5 files changed

+128
-0
lines changed

lib/mod.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,20 @@ export async function setup(document: Document, target: HTMLElement, backend: Ba
238238
}
239239
});
240240

241+
workbench.commands.registerCommand({
242+
id: "view-cards",
243+
title: "View as Cards",
244+
when: (ctx: Context) => {
245+
if (!ctx.node) return false;
246+
if (ctx.node.raw.Rel === "Fields") return false;
247+
if (ctx.node.parent && ctx.node.parent.hasComponent(Document)) return false;
248+
return true;
249+
},
250+
action: (ctx: Context) => {
251+
ctx.node.setAttr("view", "cards");
252+
}
253+
});
254+
241255

242256
workbench.commands.registerCommand({
243257
id: "add-checkbox",

lib/model/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export interface Node {
6262
removeLinked(rel: string, node: Node): void;
6363
moveLinked(rel: string, node: Node, idx: number): void;
6464

65+
componentField(name: string): any|null;
66+
6567
getAttr(name: string): string;
6668
setAttr(name: string, value: string): void;
6769

lib/model/module/node.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@ export class Node {
224224
return this.raw.Linked.Components.length;
225225
}
226226

227+
componentField(name: string): any|null {
228+
for (const com of this.components) {
229+
if (Object.keys(com.value||{}).includes(name)) {
230+
return com.value[name];
231+
}
232+
}
233+
return null;
234+
}
235+
227236
addComponent(obj: any) {
228237
const node = this.bus.make(componentName(obj), obj);
229238
node.raw.Parent = this.id;

lib/view/cards.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Node } from "../model/mod.ts";
2+
import { InlineFrame } from "../com/iframe.tsx";
3+
4+
function tryFields(n: Node, fields: string[]): any|null {
5+
for (const field of fields) {
6+
const value = n.componentField(field);
7+
if (value) {
8+
return value;
9+
}
10+
}
11+
return null;
12+
}
13+
14+
export default {
15+
view({attrs: {workbench, path}}) {
16+
const node = path.node;
17+
return (
18+
<div class="cards-view flex flex-row" style={{gap: "1rem", paddingBottom: "1rem", flexWrap: "wrap"}}>
19+
{node.children.map(n => {
20+
const linkURL = tryFields(n, ["linkURL"]);
21+
const dateTime = tryFields(n, ["updatedAt", "createdAt"]);
22+
const userName = tryFields(n, ["updatedBy", "createdBy", "username"]);
23+
const thumbnailURL = tryFields(n, ["thumbnailURL", "coverURL"]);
24+
const frame = n.getComponent(InlineFrame);
25+
26+
let thumbnail = <img style={{
27+
objectFit: "fill",
28+
objectPosition: "center",
29+
width: "12rem",
30+
height: "9rem"
31+
}} src={thumbnailURL} />;
32+
if (frame) {
33+
thumbnail = (
34+
<div style={{
35+
width: "120rem",
36+
height: "90rem",
37+
transform: "scale(0.1)",
38+
pointerEvents: "none",
39+
transformOrigin: "0 0"
40+
}}>
41+
<iframe src={frame.url} style={{
42+
border: "0",
43+
width: "100%",
44+
height: "100%"
45+
}}></iframe>
46+
</div>
47+
)
48+
}
49+
return (
50+
<div style={{border: "1px solid gray", overflow: "hidden", borderRadius: "0.5rem", paddingBottom: "0.5rem", width: "12rem"}}>
51+
<div style={{position: "relative", overflow: "hidden", width: "12rem", height: "9rem"}}>
52+
{(linkURL)
53+
? <a href={linkURL}>{thumbnail}</a>
54+
: thumbnail
55+
}
56+
</div>
57+
<div style={{padding: "0.5rem", paddingBottom: "0.25rem"}}>
58+
{(linkURL)
59+
? <a href={linkURL}>{n.name}</a>
60+
: n.name}
61+
</div>
62+
{userName && <div style={{padding: "0.5rem", paddingTop: "0", paddingBottom: "0.75rem", color: "#aaa"}}>
63+
{userName}
64+
</div>}
65+
{dateTime && <div style={{padding: "0.5rem", paddingTop: "0", paddingBottom: "0.75rem", color: "#aaa"}}>
66+
{timeAgo(dateTime)}
67+
</div>}
68+
</div>
69+
)})}
70+
</div>
71+
)
72+
}
73+
}
74+
75+
function timeAgo(date) {
76+
if (!(date instanceof Date)) {
77+
throw new Error("Input must be a valid Date object.");
78+
}
79+
80+
const now = new Date();
81+
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
82+
83+
const intervals = {
84+
year: 31536000,
85+
month: 2592000,
86+
week: 604800,
87+
day: 86400,
88+
hour: 3600,
89+
minute: 60,
90+
second: 1
91+
};
92+
93+
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
94+
const count = Math.floor(seconds / secondsInUnit);
95+
if (count > 0) {
96+
return `${count} ${unit}${count > 1 ? 's' : ''} ago`;
97+
}
98+
}
99+
100+
return 'just now';
101+
}

lib/view/views.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import list from "./list.tsx";
33
import table from "./table.tsx";
44
import tabs from "./tabs.tsx";
55
import document from "./document.tsx";
6+
import cards from "./cards.tsx";
67

78
export const views = {
89
list,
910
table,
1011
tabs,
1112
document,
13+
cards
1214
}
1315

1416
// deprecated. use getNodeView

0 commit comments

Comments
 (0)