+1
-1
deno.json
+1
-1
deno.json
···
2
2
"imports": {
3
3
"$lexicon/": "./__generated__/",
4
4
"@atproto/syntax": "npm:@atproto/syntax@^0.4.0",
5
-
"@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.8",
5
+
"@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.9",
6
6
"@gfx/canvas": "jsr:@gfx/canvas@^0.5.8",
7
7
"@std/path": "jsr:@std/path@^1.0.9",
8
8
"@tailwindcss/cli": "npm:@tailwindcss/cli@^4.1.4",
+6
-7
deno.lock
+6
-7
deno.lock
···
2
2
"version": "4",
3
3
"specifiers": {
4
4
"jsr:@bigmoves/atproto-oauth-client@0.1": "0.1.0",
5
-
"jsr:@bigmoves/bff@0.3.0-beta.8": "0.3.0-beta.8",
5
+
"jsr:@bigmoves/bff@0.3.0-beta.9": "0.3.0-beta.9",
6
6
"jsr:@denosaurs/plug@1": "1.0.5",
7
7
"jsr:@denosaurs/plug@1.0.5": "1.0.5",
8
8
"jsr:@gfx/canvas@~0.5.8": "0.5.8",
9
9
"jsr:@std/assert@0.214": "0.214.0",
10
10
"jsr:@std/assert@0.217": "0.217.0",
11
-
"jsr:@std/assert@^1.0.12": "1.0.13",
11
+
"jsr:@std/assert@^1.0.13": "1.0.13",
12
12
"jsr:@std/cache@0.2": "0.2.0",
13
13
"jsr:@std/cli@^1.0.17": "1.0.17",
14
14
"jsr:@std/encoding@0.214": "0.214.0",
···
70
70
"npm:jose"
71
71
]
72
72
},
73
-
"@bigmoves/bff@0.3.0-beta.8": {
74
-
"integrity": "9f4193d02b9ff1a16f600fbb1ad5323c75285848457429172914a2b797787055",
73
+
"@bigmoves/bff@0.3.0-beta.9": {
74
+
"integrity": "8d2f37eeb3f006670255e2c4e99e4556f686bc8c2ea009287835666cc9c0452b",
75
75
"dependencies": [
76
76
"jsr:@bigmoves/atproto-oauth-client",
77
-
"jsr:@std/assert@^1.0.12",
77
+
"jsr:@std/assert@^1.0.13",
78
78
"jsr:@std/cache",
79
79
"jsr:@std/http",
80
80
"jsr:@std/path@^1.0.8",
···
88
88
"npm:multiformats@^13.3.2",
89
89
"npm:preact",
90
90
"npm:preact-render-to-string",
91
-
"npm:sharp",
92
91
"npm:tailwind-merge"
93
92
]
94
93
},
···
1609
1608
},
1610
1609
"workspace": {
1611
1610
"dependencies": [
1612
-
"jsr:@bigmoves/bff@0.3.0-beta.8",
1611
+
"jsr:@bigmoves/bff@0.3.0-beta.9",
1613
1612
"jsr:@gfx/canvas@~0.5.8",
1614
1613
"jsr:@std/path@^1.0.9",
1615
1614
"npm:@atproto/syntax@0.4",
+1
fly.toml
+1
fly.toml
+114
-106
main.tsx
+114
-106
main.tsx
···
40
40
Layout,
41
41
Login,
42
42
Meta,
43
-
type MetaProps,
43
+
type MetaDescriptor,
44
44
Textarea,
45
45
} from "@bigmoves/bff/components";
46
46
import { createCanvas, Image } from "@gfx/canvas";
···
48
48
import { formatDistanceStrict } from "date-fns";
49
49
import { wrap } from "popmotion";
50
50
import { ComponentChildren, JSX, VNode } from "preact";
51
+
52
+
const PUBLIC_URL = Deno.env.get("BFF_PUBLIC_URL") ?? "http://localhost:8080";
53
+
const GOATCOUNTER_URL = Deno.env.get("GOATCOUNTER_URL");
51
54
52
55
let cssContentHash: string = "";
53
56
···
67
70
const cssFileContent = await Deno.readFile(
68
71
join(Deno.cwd(), "static", "styles.css"),
69
72
);
70
-
const hashBuffer = await crypto.subtle.digest(
71
-
"SHA-256",
72
-
cssFileContent,
73
-
);
73
+
const hashBuffer = await crypto.subtle.digest("SHA-256", cssFileContent);
74
74
cssContentHash = Array.from(new Uint8Array(hashBuffer))
75
75
.map((b) => b.toString(16).padStart(2, "0"))
76
76
.join("");
···
118
118
}),
119
119
route("/", (_req, _params, ctx) => {
120
120
const items = getTimeline(ctx);
121
+
ctx.state.meta = getPageMeta("");
121
122
return ctx.render(<Timeline items={items} />);
122
123
}),
123
124
route("/profile/:handle", (req, params, ctx) => {
···
130
131
if (!actor) return ctx.next();
131
132
const profile = getActorProfile(actor.did, ctx);
132
133
if (!profile) return ctx.next();
134
+
ctx.state.meta = getPageMeta(profileLink(handle));
133
135
if (tab) {
134
136
return ctx.html(
135
137
<ProfilePage
···
159
161
favs = getGalleryFavs(gallery.uri, ctx);
160
162
}
161
163
if (!gallery) return ctx.next();
162
-
ctx.state.meta = getGalleryMeta(gallery);
164
+
ctx.state.meta = [
165
+
...getPageMeta(galleryLink(handle, rkey)),
166
+
...getGalleryMeta(gallery),
167
+
];
163
168
ctx.state.scripts = ["photo_dialog.js", "masonry.js"];
164
169
return ctx.render(
165
170
<GalleryPage favs={favs} gallery={gallery} currentUserDid={did} />,
···
168
173
route("/upload", (_req, _params, ctx) => {
169
174
requireAuth(ctx);
170
175
const photos = getActorPhotos(ctx.currentUser.did, ctx);
171
-
return ctx.render(
172
-
<UploadPage photos={photos} />,
173
-
);
176
+
ctx.state.meta = getPageMeta("/upload");
177
+
return ctx.render(<UploadPage photos={photos} />);
174
178
}),
175
179
route("/dialogs/gallery/new", (_req, _params, ctx) => {
176
180
requireAuth(ctx);
177
-
return ctx.html(
178
-
<GalleryCreateEditDialog />,
179
-
);
181
+
return ctx.html(<GalleryCreateEditDialog />);
180
182
}),
181
183
route("/dialogs/gallery/:rkey", (_req, params, ctx) => {
182
184
requireAuth(ctx);
183
185
const handle = ctx.currentUser.handle;
184
186
const rkey = params.rkey;
185
187
const gallery = getGallery(handle, rkey, ctx);
186
-
return ctx.html(
187
-
<GalleryCreateEditDialog gallery={gallery} />,
188
-
);
188
+
return ctx.html(<GalleryCreateEditDialog gallery={gallery} />);
189
189
}),
190
190
route("/onboard", (_req, _params, ctx) => {
191
191
requireAuth(ctx);
···
237
237
const image = gallery.items.filter(isPhotoView).find((item) => {
238
238
return item.cid === imageCid;
239
239
});
240
-
const imageAtIndex = gallery.items.filter(isPhotoView).findIndex(
241
-
(image) => {
240
+
const imageAtIndex = gallery.items
241
+
.filter(isPhotoView)
242
+
.findIndex((image) => {
242
243
return image.cid === imageCid;
243
-
},
244
-
);
244
+
});
245
245
const next = wrap(0, gallery.items.length, imageAtIndex + 1);
246
246
const prev = wrap(0, gallery.items.length, imageAtIndex - 1);
247
247
if (!image) return ctx.next();
···
354
354
const photo = ctx.indexService.getRecord<WithBffMeta<Photo>>(photoUri);
355
355
if (!gallery || !photo) return ctx.next();
356
356
if (
357
-
gallery.items?.filter(isPhotoView).some((item) =>
358
-
item.uri === photoUri
359
-
)
357
+
gallery.items
358
+
?.filter(isPhotoView)
359
+
.some((item) => item.uri === photoUri)
360
360
) {
361
361
return new Response(null, { status: 500 });
362
362
}
363
-
await ctx.createRecord<Gallery>(
364
-
"social.grain.gallery.item",
365
-
{
366
-
gallery: galleryUri,
367
-
item: photoUri,
368
-
createdAt: new Date().toISOString(),
369
-
},
370
-
);
363
+
await ctx.createRecord<Gallery>("social.grain.gallery.item", {
364
+
gallery: galleryUri,
365
+
item: photoUri,
366
+
createdAt: new Date().toISOString(),
367
+
});
371
368
gallery.items = [
372
369
...(gallery.items ?? []),
373
370
photoToView(photo.did, photo),
···
408
405
if (!galleryRkey || !photoRkey) return ctx.next();
409
406
const photo = ctx.indexService.getRecord<WithBffMeta<Photo>>(photoUri);
410
407
if (!photo) return ctx.next();
411
-
const { items: [item] } = ctx.indexService.getRecords<
412
-
WithBffMeta<GalleryItem>
413
-
>(
408
+
const {
409
+
items: [item],
410
+
} = ctx.indexService.getRecords<WithBffMeta<GalleryItem>>(
414
411
"social.grain.gallery.item",
415
412
{
416
413
where: [
···
426
423
},
427
424
);
428
425
if (!item) return ctx.next();
429
-
await ctx.deleteRecord(
430
-
item.uri,
431
-
);
426
+
await ctx.deleteRecord(item.uri);
432
427
const gallery = getGallery(ctx.currentUser.did, galleryRkey, ctx);
433
428
if (!gallery) return ctx.next();
434
429
return ctx.html(
···
479
474
);
480
475
}
481
476
482
-
await ctx.createRecord<WithBffMeta<Favorite>>(
483
-
"social.grain.favorite",
484
-
{
485
-
subject: galleryUri,
486
-
createdAt: new Date().toISOString(),
487
-
},
488
-
);
477
+
await ctx.createRecord<WithBffMeta<Favorite>>("social.grain.favorite", {
478
+
subject: galleryUri,
479
+
createdAt: new Date().toISOString(),
480
+
});
489
481
490
482
const favs = getGalleryFavs(galleryUri, ctx);
491
483
···
535
527
type State = {
536
528
profile?: ProfileView;
537
529
scripts?: string[];
538
-
meta?: MetaProps[];
530
+
meta?: MetaDescriptor[];
539
531
};
540
532
541
533
function readFileAsDataURL(file: File): Promise<string> {
···
614
606
ctx: BffContext,
615
607
galleries: WithBffMeta<Gallery>[],
616
608
): Map<string, WithBffMeta<Photo>[]> {
617
-
const galleryUris = galleries.map((gallery) =>
618
-
`at://${gallery.did}/social.grain.gallery/${new AtUri(gallery.uri).rkey}`
609
+
const galleryUris = galleries.map(
610
+
(gallery) =>
611
+
`at://${gallery.did}/social.grain.gallery/${new AtUri(gallery.uri).rkey}`,
619
612
);
620
613
621
614
if (galleryUris.length === 0) return new Map();
···
852
845
}
853
846
const { items: galleries } = ctx.indexService.getRecords<
854
847
WithBffMeta<Gallery>
855
-
>(
856
-
"social.grain.gallery",
857
-
{
858
-
where: [{ field: "did", equals: did }],
859
-
orderBy: { field: "createdAt", direction: "desc" },
860
-
},
861
-
);
848
+
>("social.grain.gallery", {
849
+
where: [{ field: "did", equals: did }],
850
+
orderBy: { field: "createdAt", direction: "desc" },
851
+
});
862
852
const galleryPhotosMap = getGalleryItemsAndPhotos(ctx, galleries);
863
853
const creator = getActorProfile(did, ctx);
864
854
if (!creator) return [];
···
906
896
return results.items;
907
897
}
908
898
909
-
function getGalleryMeta(gallery: GalleryView): MetaProps[] {
899
+
function getPageMeta(pageUrl: string): MetaDescriptor[] {
910
900
return [
911
-
{ property: "og:type", content: "website" },
912
-
{ property: "og:site_name", content: "Atproto Image Gallery" },
901
+
{
902
+
tagName: "link",
903
+
property: "canonical",
904
+
href: `${PUBLIC_URL}${pageUrl}`,
905
+
},
906
+
{ property: "og:site_name", content: "Grain Social" },
907
+
];
908
+
}
909
+
910
+
function getGalleryMeta(gallery: GalleryView): MetaDescriptor[] {
911
+
return [
912
+
// { property: "og:type", content: "website" },
913
913
{
914
914
property: "og:url",
915
-
content: `${
916
-
Deno.env.get("BFF_PUBLIC_URL") ?? "http://localhost:8080"
917
-
}/profile/${gallery.creator.handle}/${new AtUri(gallery.uri).rkey}`,
915
+
content: `${PUBLIC_URL}/profile/${gallery.creator.handle}/${
916
+
new AtUri(gallery.uri).rkey
917
+
}`,
918
918
},
919
919
{ property: "og:title", content: (gallery.record as Gallery).title },
920
920
{
···
937
937
<meta charset="UTF-8" />
938
938
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
939
939
<Meta meta={props.ctx.state.meta} />
940
+
{GOATCOUNTER_URL
941
+
? (
942
+
<script
943
+
data-goatcounter={GOATCOUNTER_URL}
944
+
async
945
+
src="//gc.zgo.at/count.js"
946
+
/>
947
+
)
948
+
: null}
940
949
<script src="https://unpkg.com/htmx.org@1.9.10" />
941
950
<script src="https://unpkg.com/hyperscript.org@0.9.14" />
942
951
<style dangerouslySetInnerHTML={{ __html: CSS }} />
···
961
970
<body class="h-full w-full dark:bg-zinc-950 dark:text-white">
962
971
<Layout id="layout" class="dark:border-zinc-800">
963
972
<Layout.Nav
964
-
title={
973
+
heading={
965
974
<h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white">
966
975
grain
967
976
<sub class="bottom-[0.75rem] text-[1rem]">beta</sub>
···
1217
1226
? (
1218
1227
<ul class="space-y-4 relative">
1219
1228
{timelineItems.length
1220
-
? timelineItems.map((item) => (
1221
-
<TimelineItem item={item} key={item.itemUri} />
1222
-
))
1229
+
? (
1230
+
timelineItems.map((item) => (
1231
+
<TimelineItem item={item} key={item.itemUri} />
1232
+
))
1233
+
)
1223
1234
: <li>No activity yet.</li>}
1224
1235
</ul>
1225
1236
)
···
1477
1488
_="on load or htmx:afterSettle call computeMasonry()"
1478
1489
>
1479
1490
{gallery.items?.filter(isPhotoView)?.length
1480
-
? gallery?.items?.filter(isPhotoView)?.map((photo) => (
1481
-
<PhotoButton
1482
-
key={photo.cid}
1483
-
photo={photo}
1484
-
gallery={gallery}
1485
-
isCreator={isCreator}
1486
-
isLoggedIn={isLoggedIn}
1487
-
/>
1488
-
))
1491
+
? gallery?.items
1492
+
?.filter(isPhotoView)
1493
+
?.map((photo) => (
1494
+
<PhotoButton
1495
+
key={photo.cid}
1496
+
photo={photo}
1497
+
gallery={gallery}
1498
+
isCreator={isCreator}
1499
+
isLoggedIn={isLoggedIn}
1500
+
/>
1501
+
))
1489
1502
: null}
1490
1503
</div>
1491
1504
</div>
1492
1505
);
1493
1506
}
1494
1507
1495
-
function PhotoButton({ photo, gallery, isCreator, isLoggedIn }: Readonly<{
1508
+
function PhotoButton({
1509
+
photo,
1510
+
gallery,
1511
+
isCreator,
1512
+
isLoggedIn,
1513
+
}: Readonly<{
1496
1514
photo: PhotoView;
1497
1515
gallery: GalleryView;
1498
1516
isCreator: boolean;
···
1794
1812
<Button type="submit" variant="primary" class="w-full">
1795
1813
Save
1796
1814
</Button>
1797
-
<Dialog.Close class="w-full">
1798
-
Cancel
1799
-
</Dialog.Close>
1815
+
<Dialog.Close class="w-full">Cancel</Dialog.Close>
1800
1816
</div>
1801
1817
</form>
1802
1818
</Dialog.Content>
···
1828
1844
))}
1829
1845
</div>
1830
1846
<div class="w-full flex flex-col gap-2 mt-2">
1831
-
<Dialog.Close class="w-full">
1832
-
Close
1833
-
</Dialog.Close>
1847
+
<Dialog.Close class="w-full">Close</Dialog.Close>
1834
1848
</div>
1835
1849
</Dialog.Content>
1836
1850
</Dialog>
···
1896
1910
cid: record.cid,
1897
1911
creator,
1898
1912
record,
1899
-
items: items?.map((item) => itemToView(record.did, item)).filter(
1900
-
isPhotoView,
1901
-
),
1913
+
items: items
1914
+
?.map((item) => itemToView(record.did, item))
1915
+
.filter(isPhotoView),
1902
1916
indexedAt: record.indexedAt,
1903
1917
};
1904
1918
}
1905
1919
1906
1920
function itemToView(
1907
1921
did: string,
1908
-
item: WithBffMeta<Photo> | {
1909
-
$type: string;
1910
-
},
1922
+
item:
1923
+
| WithBffMeta<Photo>
1924
+
| {
1925
+
$type: string;
1926
+
},
1911
1927
): Un$Typed<PhotoView> | undefined {
1912
1928
if (isPhoto(item)) {
1913
1929
return photoToView(did, item);
···
2092
2108
if (!uploadId) return ctx.next();
2093
2109
const meta = ctx.blobMetaCache.get(uploadId);
2094
2110
if (!meta?.dataUrl || !meta?.blobRef) return ctx.next();
2095
-
const photoUri = await ctx.createRecord<Photo>(
2096
-
"social.grain.photo",
2097
-
{
2098
-
photo: meta.blobRef,
2099
-
aspectRatio: meta.dimensions?.width && meta.dimensions?.height
2100
-
? {
2101
-
width: meta.dimensions.width,
2102
-
height: meta.dimensions.height,
2103
-
}
2104
-
: undefined,
2105
-
alt: "",
2106
-
createdAt: new Date().toISOString(),
2107
-
},
2108
-
);
2109
-
return ctx.html(
2110
-
cb({ dataUrl: meta.dataUrl, uri: photoUri }),
2111
-
);
2111
+
const photoUri = await ctx.createRecord<Photo>("social.grain.photo", {
2112
+
photo: meta.blobRef,
2113
+
aspectRatio: meta.dimensions?.width && meta.dimensions?.height
2114
+
? {
2115
+
width: meta.dimensions.width,
2116
+
height: meta.dimensions.height,
2117
+
}
2118
+
: undefined,
2119
+
alt: "",
2120
+
createdAt: new Date().toISOString(),
2121
+
});
2122
+
return ctx.html(cb({ dataUrl: meta.dataUrl, uri: photoUri }));
2112
2123
};
2113
2124
}
2114
2125
···
2136
2147
`/actions/photo/upload-done`,
2137
2148
["GET"],
2138
2149
photoUploadDone(({ dataUrl, uri }) => (
2139
-
<PhotoPreview
2140
-
src={dataUrl}
2141
-
uri={uri}
2142
-
/>
2150
+
<PhotoPreview src={dataUrl} uri={uri} />
2143
2151
)),
2144
2152
),
2145
2153
];
-2
static/styles.css
-2
static/styles.css
···
8
8
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
9
9
"Courier New", monospace;
10
10
--color-sky-500: oklch(68.5% 0.169 237.323);
11
-
--color-slate-800: oklch(27.9% 0.041 260.031);
12
-
--color-slate-900: oklch(20.8% 0.042 265.755);
13
11
--color-zinc-100: oklch(96.7% 0.001 286.375);
14
12
--color-zinc-200: oklch(92% 0.004 286.32);
15
13
--color-zinc-500: oklch(55.2% 0.016 285.938);