+4
__generated__/lexicons.ts
+4
__generated__/lexicons.ts
+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.14",
5
+
"@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.15",
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",
+4
-4
deno.lock
+4
-4
deno.lock
···
2
2
"version": "4",
3
3
"specifiers": {
4
4
"jsr:@bigmoves/atproto-oauth-client@0.2": "0.2.0",
5
-
"jsr:@bigmoves/bff@0.3.0-beta.14": "0.3.0-beta.14",
5
+
"jsr:@bigmoves/bff@0.3.0-beta.15": "0.3.0-beta.15",
6
6
"jsr:@deno/gfm@0.10": "0.10.0",
7
7
"jsr:@denosaurs/emoji@0.3": "0.3.1",
8
8
"jsr:@denosaurs/plug@1": "1.0.5",
···
92
92
"npm:jose"
93
93
]
94
94
},
95
-
"@bigmoves/bff@0.3.0-beta.14": {
96
-
"integrity": "2b94d1f58c9b035cb2a50e3161953ab5c8c158caf902eccd89ae0beb2db60edc",
95
+
"@bigmoves/bff@0.3.0-beta.15": {
96
+
"integrity": "934d0fab8cc73804099ccb5362fa89f5ef3cd6269a6613029131770c97cdfcb9",
97
97
"dependencies": [
98
98
"jsr:@bigmoves/atproto-oauth-client",
99
99
"jsr:@std/assert@^1.0.13",
···
1810
1810
},
1811
1811
"workspace": {
1812
1812
"dependencies": [
1813
-
"jsr:@bigmoves/bff@0.3.0-beta.14",
1813
+
"jsr:@bigmoves/bff@0.3.0-beta.15",
1814
1814
"jsr:@gfx/canvas@~0.5.8",
1815
1815
"jsr:@std/path@^1.0.9",
1816
1816
"npm:@atproto/syntax@0.4",
+125
-59
main.tsx
+125
-59
main.tsx
···
58
58
const PUBLIC_URL = Deno.env.get("BFF_PUBLIC_URL") ?? "http://localhost:8080";
59
59
const GOATCOUNTER_URL = Deno.env.get("GOATCOUNTER_URL");
60
60
61
-
let cssContentHash: string = "";
62
-
const staticJsFiles = new Map<string, string>();
61
+
const staticFilesHash = new Map<string, string>();
63
62
64
63
bff({
65
64
appName: "Grain Social",
···
74
73
lexicons,
75
74
rootElement: Root,
76
75
onListen: async () => {
77
-
const cssFileContent = await Deno.readFile(
78
-
join(Deno.cwd(), "static", "styles.css"),
79
-
);
80
-
const hashBuffer = await crypto.subtle.digest("SHA-256", cssFileContent);
81
-
cssContentHash = Array.from(new Uint8Array(hashBuffer))
82
-
.map((b) => b.toString(16).padStart(2, "0"))
83
-
.join("");
84
76
for (const entry of Deno.readDirSync(join(Deno.cwd(), "static"))) {
85
-
if (entry.isFile && entry.name.endsWith(".js")) {
77
+
if (
78
+
entry.isFile &&
79
+
(entry.name.endsWith(".js") || entry.name.endsWith(".css"))
80
+
) {
86
81
const fileContent = await Deno.readFile(
87
82
join(Deno.cwd(), "static", entry.name),
88
83
);
···
90
85
const hash = Array.from(new Uint8Array(hashBuffer))
91
86
.map((b) => b.toString(16).padStart(2, "0"))
92
87
.join("");
93
-
staticJsFiles.set(entry.name, hash);
88
+
staticFilesHash.set(entry.name, hash);
94
89
}
95
90
}
96
91
},
···
250
245
const rkey = params.rkey;
251
246
const gallery = getGallery(handle, rkey, ctx);
252
247
return ctx.html(<GalleryCreateEditDialog gallery={gallery} />);
248
+
}),
249
+
route("/dialogs/gallery/:rkey/sort", (_req, params, ctx) => {
250
+
requireAuth(ctx);
251
+
const handle = ctx.currentUser.handle;
252
+
const rkey = params.rkey;
253
+
const gallery = getGallery(handle, rkey, ctx);
254
+
if (!gallery) return ctx.next();
255
+
return ctx.html(<GallerySortDialog gallery={gallery} />);
253
256
}),
254
257
route("/onboard", (_req, _params, ctx) => {
255
258
requireAuth(ctx);
···
575
578
576
579
return ctx.redirect(`/profile/${ctx.currentUser.handle}`);
577
580
}),
578
-
route("/actions/sort-end", ["POST"], async (req, _params, ctx) => {
579
-
const formData = await req.formData();
580
-
const items = formData.getAll("item") as string[];
581
-
console.log(items);
582
-
return new Response(null, { status: 200 });
583
-
}),
581
+
route(
582
+
"/actions/gallery/:rkey/sort",
583
+
["POST"],
584
+
async (req, params, ctx) => {
585
+
requireAuth(ctx);
586
+
const galleryRkey = params.rkey;
587
+
const galleryUri =
588
+
`at://${ctx.currentUser.did}/social.grain.gallery/${galleryRkey}`;
589
+
const {
590
+
items,
591
+
} = ctx.indexService.getRecords<WithBffMeta<GalleryItem>>(
592
+
"social.grain.gallery.item",
593
+
{
594
+
where: [
595
+
{
596
+
field: "gallery",
597
+
equals: galleryUri,
598
+
},
599
+
],
600
+
},
601
+
);
602
+
const itemsMap = new Map<string, WithBffMeta<GalleryItem>>();
603
+
for (const item of items) {
604
+
itemsMap.set(item.item, item);
605
+
}
606
+
const formData = await req.formData();
607
+
const sortedItems = formData.getAll("item") as string[];
608
+
const updates = [];
609
+
let position = 0;
610
+
for (const sortedItemUri of sortedItems) {
611
+
const item = itemsMap.get(sortedItemUri);
612
+
if (!item) continue;
613
+
updates.push({
614
+
collection: "social.grain.gallery.item",
615
+
rkey: new AtUri(item.uri).rkey,
616
+
data: {
617
+
gallery: item.gallery,
618
+
item: item.item,
619
+
createdAt: item.createdAt,
620
+
position,
621
+
},
622
+
});
623
+
position++;
624
+
}
625
+
await ctx.updateRecords<WithBffMeta<GalleryItem>>(updates);
626
+
return ctx.redirect(
627
+
`/profile/${ctx.currentUser.handle}/${galleryRkey}`,
628
+
);
629
+
},
630
+
),
584
631
...photoUploadRoutes(),
585
632
...avatarUploadRoutes(),
586
633
],
···
699
746
const { items: galleryItems } = ctx.indexService.getRecords<
700
747
WithBffMeta<GalleryItem>
701
748
>("social.grain.gallery.item", {
702
-
orderBy: { field: "createdAt", direction: "asc" },
749
+
orderBy: { field: "position", direction: "asc" },
703
750
where: [{ field: "gallery", in: galleryUris }],
704
751
});
705
752
···
1055
1102
<script src="https://unpkg.com/hyperscript.org@0.9.14" />
1056
1103
<script src="https://unpkg.com/sortablejs@1.15.6" />
1057
1104
<style dangerouslySetInnerHTML={{ __html: CSS }} />
1058
-
<link rel="stylesheet" href={`/static/styles.css?${cssContentHash}`} />
1105
+
<link
1106
+
rel="stylesheet"
1107
+
href={`/static/styles.css?${staticFilesHash.get("styles.css")}`}
1108
+
/>
1059
1109
<link rel="preconnect" href="https://fonts.googleapis.com" />
1060
1110
<link
1061
1111
rel="preconnect"
···
1074
1124
{scripts?.map((file) => (
1075
1125
<script
1076
1126
key={file}
1077
-
src={`/static/${file}?${staticJsFiles.get(file)}`}
1127
+
src={`/static/${file}?${staticFilesHash.get(file)}`}
1078
1128
/>
1079
1129
))}
1080
1130
</head>
···
1660
1710
hx-target="#layout"
1661
1711
hx-swap="afterbegin"
1662
1712
>
1663
-
Change Sort
1664
-
</Button>
1665
-
<Button
1666
-
variant="primary"
1667
-
class="self-start w-full sm:w-fit"
1668
-
hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`}
1669
-
hx-target="#layout"
1670
-
hx-swap="afterbegin"
1671
-
>
1672
1713
Edit
1673
1714
</Button>
1674
1715
<Button
···
1680
1721
>
1681
1722
Add photos
1682
1723
</Button>
1724
+
<Button
1725
+
variant="primary"
1726
+
class="self-start w-full sm:w-fit"
1727
+
hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}/sort`}
1728
+
hx-target="#layout"
1729
+
hx-swap="afterbegin"
1730
+
>
1731
+
Sort order
1732
+
</Button>
1683
1733
<ShareGalleryButton gallery={gallery} />
1684
1734
</div>
1685
1735
)
···
1697
1747
)
1698
1748
: null}
1699
1749
</div>
1700
-
<SortableGrid gallery={gallery} />
1701
-
{
1702
-
/* <div
1703
1750
<div class="flex justify-end mb-2">
1704
1751
<Button
1705
1752
id="justified-button"
···
1817
1864
/>
1818
1865
))
1819
1866
: null}
1820
-
</div> */
1821
-
}
1867
+
</div>
1822
1868
</div>
1823
1869
);
1824
1870
}
···
1858
1904
);
1859
1905
}
1860
1906
1861
-
function SortableGrid({ gallery }: Readonly<{ gallery: GalleryView }>) {
1907
+
function GallerySortDialog({ gallery }: Readonly<{ gallery: GalleryView }>) {
1862
1908
return (
1863
-
<form
1864
-
id="masonry-container"
1865
-
class="sortable h-0 overflow-hidden relative mx-auto w-full"
1866
-
_="on load or htmx:afterSettle call computeMasonry()"
1867
-
// hx-post="/actions/sort-end"
1868
-
// hx-trigger="end"
1869
-
// hx-swap="none"
1870
-
>
1871
-
<div class="htmx-indicator">Updating...</div>
1872
-
{gallery?.items?.filter(isPhotoView).map((item) => (
1873
-
<div
1874
-
key={item.cid}
1875
-
class="masonry-tile absolute cursor-pointer"
1876
-
data-width={item.aspectRatio?.width}
1877
-
data-height={item.aspectRatio?.height}
1909
+
<Dialog>
1910
+
<Dialog.Content class="dark:bg-zinc-950 relative">
1911
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
1912
+
<Dialog.Title>Sort gallery</Dialog.Title>
1913
+
<p class="my-2 text-center">Drag photos to rearrange</p>
1914
+
<form
1915
+
hx-post={`/actions/gallery/${new AtUri(gallery.uri).rkey}/sort`}
1916
+
hx-trigger="submit"
1917
+
hx-swap="none"
1878
1918
>
1879
-
<input type="hidden" name="item" value={item.uri} />
1880
-
<img
1881
-
src={item.fullsize}
1882
-
alt={item.alt}
1883
-
class="w-full h-full object-cover"
1884
-
/>
1885
-
</div>
1886
-
))}
1887
-
</form>
1919
+
<div class="sortable grid grid-cols-3 sm:grid-cols-5 gap-2 mt-2">
1920
+
{gallery?.items?.filter(isPhotoView).map((item) => (
1921
+
<div
1922
+
key={item.cid}
1923
+
class="relative aspect-square cursor-grab"
1924
+
>
1925
+
<input type="hidden" name="item" value={item.uri} />
1926
+
<img
1927
+
src={item.fullsize}
1928
+
alt={item.alt}
1929
+
class="w-full h-full absolute object-cover"
1930
+
/>
1931
+
</div>
1932
+
))}
1933
+
</div>
1934
+
<div class="flex flex-col gap-2 mt-2">
1935
+
<Button
1936
+
variant="primary"
1937
+
type="submit"
1938
+
class="w-full"
1939
+
>
1940
+
Save
1941
+
</Button>
1942
+
<Button
1943
+
variant="secondary"
1944
+
type="button"
1945
+
class="w-full"
1946
+
_={Dialog._closeOnClick}
1947
+
>
1948
+
Cancel
1949
+
</Button>
1950
+
</div>
1951
+
</form>
1952
+
</Dialog.Content>
1953
+
</Dialog>
1888
1954
);
1889
1955
}
1890
1956
+1
-21
static/sortable.js
+1
-21
static/sortable.js
···
1
1
htmx.onLoad(function (content) {
2
2
const sortables = content.querySelectorAll(".sortable");
3
3
for (const sortable of sortables) {
4
-
const sortableInstance = new Sortable(sortable, {
4
+
new Sortable(sortable, {
5
5
animation: 150,
6
-
swap: true,
7
-
swapClass: "opacity-50",
8
-
9
-
// Make the `.htmx-indicator` unsortable
10
-
filter: ".htmx-indicator",
11
-
onMove: function (evt) {
12
-
console.log("onMove", evt);
13
-
return evt.related.className.indexOf("htmx-indicator") === -1;
14
-
},
15
-
16
-
// Disable sorting on the `end` event
17
-
onEnd: function (_evt) {
18
-
console.log("onEnd");
19
-
// this.option("disabled", true);
20
-
},
21
-
});
22
-
23
-
// Re-enable sorting on the `htmx:afterSwap` event
24
-
sortable.addEventListener("htmx:afterSwap", function () {
25
-
// sortableInstance.option("disabled", false);
26
6
});
27
7
}
28
8
});
+6
-3
static/styles.css
+6
-3
static/styles.css
···
387
387
.shrink-0 {
388
388
flex-shrink: 0;
389
389
}
390
+
.cursor-grab {
391
+
cursor: grab;
392
+
}
390
393
.cursor-pointer {
391
394
cursor: pointer;
392
395
}
···
483
486
@supports (color: color-mix(in lab, red, red)) {
484
487
background-color: color-mix(in oklab, var(--color-black) 80%, transparent);
485
488
}
489
+
}
490
+
.bg-sky-500 {
491
+
background-color: var(--color-sky-500);
486
492
}
487
493
.bg-zinc-100 {
488
494
background-color: var(--color-zinc-100);
···
592
598
}
593
599
.lowercase {
594
600
text-transform: lowercase;
595
-
}
596
-
.opacity-50 {
597
-
opacity: 50%;
598
601
}
599
602
.ring-sky-500 {
600
603
--tw-ring-color: var(--color-sky-500);