Skip to content

Commit a6b644f

Browse files
committed
refactor(web): split PageEditorView into composables and cards
Extract page editor path/prefs, snapshots, management, and collab/session into dedicated composables plus presentational cards. Add shared helpers for Yjs doc creation, awareness colors, snapshot list fetch, and editor constants.
1 parent 932bfb6 commit a6b644f

14 files changed

Lines changed: 2037 additions & 1510 deletions
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<script setup lang="ts">
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
} from "@/components/ui/card";
9+
10+
defineProps<{
11+
collabLoading: boolean;
12+
loadError: string | null;
13+
cryptoError: string | null;
14+
collabWsLive: boolean;
15+
collabWsError: string | null;
16+
updateCount: number;
17+
collabLastIndex: number | null;
18+
pushError: string | null;
19+
}>();
20+
</script>
21+
22+
<template>
23+
<Card>
24+
<CardHeader>
25+
<CardTitle>Server collab</CardTitle>
26+
<CardDescription>
27+
<code
28+
class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
29+
>GET /api/pages/…/collab-updates</code>
30+
bootstraps ciphertext; live edits use
31+
<code
32+
class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
33+
>WebSocket …/collab-ws</code>
34+
(Durable Object relay + Postgres append) when configured, otherwise
35+
<code class="font-mono text-xs">POST …/collab-updates</code>.
36+
</CardDescription>
37+
</CardHeader>
38+
<CardContent class="space-y-2 text-sm">
39+
<p v-if="collabLoading" class="text-muted-foreground">Loading…</p>
40+
<template v-else>
41+
<p v-if="loadError" class="text-destructive">
42+
{{ loadError }}
43+
</p>
44+
<p v-else-if="cryptoError" class="text-amber-700 dark:text-amber-400">
45+
{{ cryptoError }}
46+
</p>
47+
<template v-else>
48+
<p>
49+
<span class="text-muted-foreground">Live collab</span>:
50+
<span :class="collabWsLive ? 'text-green-700 dark:text-green-400' : 'text-muted-foreground'">
51+
{{ collabWsLive ? "WebSocket connected" : "offline (REST only)" }}
52+
</span>
53+
</p>
54+
<p v-if="collabWsError" class="text-destructive">
55+
{{ collabWsError }}
56+
</p>
57+
<p>
58+
<span class="text-muted-foreground">Updates on server</span>:
59+
{{ updateCount }} ·
60+
<span class="text-muted-foreground">lastIndex</span>:
61+
{{ collabLastIndex === null ? "—" : collabLastIndex }}
62+
</p>
63+
<p v-if="pushError" class="text-destructive">
64+
Save error: {{ pushError }}
65+
</p>
66+
</template>
67+
</template>
68+
</CardContent>
69+
</Card>
70+
</template>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<script setup lang="ts">
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
} from "@/components/ui/card";
10+
import { Input } from "@/components/ui/input";
11+
import { Label } from "@/components/ui/label";
12+
13+
defineProps<{
14+
moveDestGroupId: string;
15+
collabLoading: boolean;
16+
loadError: string | null;
17+
cryptoError: string | null;
18+
collabGroupId: string | null;
19+
}>();
20+
21+
defineEmits<{
22+
"update:moveDestGroupId": [value: string];
23+
moveToGroup: [];
24+
setAsMainPage: [];
25+
softDelete: [];
26+
purge: [];
27+
}>();
28+
</script>
29+
30+
<template>
31+
<Card>
32+
<CardHeader>
33+
<CardTitle class="text-base">Page management</CardTitle>
34+
<CardDescription>
35+
Main-page promotion uses
36+
<code class="font-mono text-xs">POST …/move</code>
37+
with the same group id (Pro). Cross-group move re-encrypts ciphertext client-side. Soft-delete and purge use
38+
<code class="font-mono text-xs">DELETE …/pages/:id</code>
39+
and
40+
<code class="font-mono text-xs">POST …/purge</code>.
41+
</CardDescription>
42+
</CardHeader>
43+
<CardContent class="space-y-4">
44+
<div class="space-y-2">
45+
<Label for="move-dest-gid" class="text-muted-foreground text-xs">
46+
Move to group id (21-char nanoid, Pro)
47+
</Label>
48+
<div class="flex flex-wrap items-end gap-2">
49+
<Input
50+
id="move-dest-gid"
51+
class="max-w-md font-mono text-xs"
52+
placeholder="Destination group id"
53+
:disabled="collabLoading || loadError != null || cryptoError != null"
54+
:model-value="moveDestGroupId"
55+
@update:model-value="
56+
$emit('update:moveDestGroupId', $event == null ? '' : String($event))
57+
"
58+
/>
59+
<Button
60+
size="sm"
61+
variant="secondary"
62+
:disabled="collabLoading || loadError != null || cryptoError != null"
63+
@click="$emit('moveToGroup')"
64+
>
65+
Move to group…
66+
</Button>
67+
</div>
68+
</div>
69+
<div class="flex flex-wrap gap-2">
70+
<Button
71+
size="sm"
72+
variant="secondary"
73+
:disabled="collabGroupId == null || collabLoading"
74+
@click="$emit('setAsMainPage')"
75+
>
76+
Set as group main page
77+
</Button>
78+
<Button
79+
size="sm"
80+
variant="destructive"
81+
@click="$emit('softDelete')"
82+
>
83+
Soft-delete page…
84+
</Button>
85+
<Button
86+
size="sm"
87+
variant="destructive"
88+
@click="$emit('purge')"
89+
>
90+
Purge page permanently…
91+
</Button>
92+
</div>
93+
</CardContent>
94+
</Card>
95+
</template>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script setup lang="ts">
2+
import { RouterLink } from "vue-router";
3+
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card";
12+
13+
defineProps<{
14+
pathLoading: boolean;
15+
pathError: string | null;
16+
pathPageIds: string[];
17+
pagePrefsLoading: boolean;
18+
isFavorite: boolean;
19+
bumpMessage: string | null;
20+
favoriteMessage: string | null;
21+
isDemo: boolean;
22+
}>();
23+
24+
defineEmits<{
25+
bumpAsStarting: [];
26+
toggleFavorite: [];
27+
removeFromRecent: [];
28+
}>();
29+
</script>
30+
31+
<template>
32+
<Card>
33+
<CardHeader>
34+
<CardTitle class="text-base">Path and prefs</CardTitle>
35+
<CardDescription>
36+
Breadcrumb toward your personal main page, plus starting-page bump and favorites (legacy
37+
<code class="font-mono text-xs">users.pages</code> / <code class="font-mono text-xs">pages.bump</code>).
38+
</CardDescription>
39+
</CardHeader>
40+
<CardContent class="space-y-3 text-sm">
41+
<p v-if="pathLoading" class="text-muted-foreground">Loading path…</p>
42+
<p v-else-if="pathError" class="text-destructive">{{ pathError }}</p>
43+
<nav v-else class="text-muted-foreground flex flex-wrap items-center gap-1 text-xs">
44+
<template v-for="(pid, i) in pathPageIds" :key="pid">
45+
<span v-if="i > 0" aria-hidden="true">/</span>
46+
<RouterLink
47+
v-if="i < pathPageIds.length - 1"
48+
class="text-primary font-mono underline"
49+
:to="`/pages/${pid}`"
50+
>{{ pid.slice(0, 8) }}…</RouterLink>
51+
<span v-else class="text-foreground font-mono font-medium">{{ pid }}</span>
52+
</template>
53+
</nav>
54+
<div class="flex flex-wrap gap-2">
55+
<Button
56+
size="sm"
57+
variant="secondary"
58+
:disabled="isDemo || pagePrefsLoading"
59+
@click="$emit('bumpAsStarting')"
60+
>
61+
Make starting page
62+
</Button>
63+
<Button
64+
size="sm"
65+
variant="outline"
66+
:disabled="isDemo || pagePrefsLoading"
67+
@click="$emit('toggleFavorite')"
68+
>
69+
{{ isFavorite ? "Remove favorite" : "Add favorite" }}
70+
</Button>
71+
<Button
72+
size="sm"
73+
variant="ghost"
74+
:disabled="pagePrefsLoading"
75+
@click="$emit('removeFromRecent')"
76+
>
77+
Remove from recent
78+
</Button>
79+
</div>
80+
<p v-if="bumpMessage" class="text-muted-foreground text-xs">{{ bumpMessage }}</p>
81+
<p v-if="favoriteMessage" class="text-amber-800 dark:text-amber-200 text-xs">{{ favoriteMessage }}</p>
82+
<p v-if="isDemo" class="text-muted-foreground text-xs">
83+
Demo accounts cannot bump starting page or favorites.
84+
</p>
85+
</CardContent>
86+
</Card>
87+
</template>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script setup lang="ts">
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
} from "@/components/ui/card";
10+
11+
import type { SnapshotRow } from "./page-snapshot-list";
12+
13+
defineProps<{
14+
snapshotLoading: boolean;
15+
snapshots: SnapshotRow[];
16+
collabLoading: boolean;
17+
loadError: string | null;
18+
cryptoError: string | null;
19+
}>();
20+
21+
defineEmits<{
22+
restore: [snapshotId: string];
23+
remove: [snapshotId: string];
24+
saveManual: [];
25+
}>();
26+
</script>
27+
28+
<template>
29+
<Card>
30+
<CardHeader>
31+
<CardTitle class="text-base">Snapshots (Pro)</CardTitle>
32+
<CardDescription>
33+
Encrypted Yjs checkpoints (
34+
<code class="font-mono text-xs">PageSnapshotData</code>
35+
). Requires edit access + subscription server-side.
36+
</CardDescription>
37+
</CardHeader>
38+
<CardContent class="space-y-3 text-sm">
39+
<p v-if="snapshotLoading" class="text-muted-foreground">Loading snapshots…</p>
40+
<p v-else-if="snapshots.length === 0" class="text-muted-foreground">No snapshots yet.</p>
41+
<ul v-else class="space-y-2">
42+
<li
43+
v-for="s in snapshots"
44+
:key="s.snapshotId"
45+
class="flex flex-col gap-2 rounded-md border p-3 sm:flex-row sm:items-center sm:justify-between"
46+
>
47+
<div class="text-xs">
48+
<div class="font-mono font-medium">{{ s.snapshotId }}</div>
49+
<div class="text-muted-foreground">
50+
{{ s.type }} · {{ s.creationDate }}
51+
</div>
52+
</div>
53+
<div class="flex flex-wrap gap-2">
54+
<Button
55+
size="sm"
56+
variant="secondary"
57+
:disabled="collabLoading || loadError != null || cryptoError != null"
58+
@click="$emit('restore', s.snapshotId)"
59+
>
60+
Restore
61+
</Button>
62+
<Button
63+
size="sm"
64+
variant="outline"
65+
@click="$emit('remove', s.snapshotId)"
66+
>
67+
Delete
68+
</Button>
69+
</div>
70+
</li>
71+
</ul>
72+
<Button
73+
size="sm"
74+
:disabled="collabLoading || loadError != null || cryptoError != null"
75+
@click="$emit('saveManual')"
76+
>
77+
Save snapshot
78+
</Button>
79+
</CardContent>
80+
</Card>
81+
</template>

0 commit comments

Comments
 (0)