grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip

+158 -3
+4
lexicons/social/grain/gallery/item.json
··· 20 20 "item": { 21 21 "type": "string", 22 22 "format": "at-uri" 23 + }, 24 + "sort": { 25 + "type": "integer", 26 + "default": 0 23 27 } 24 28 } 25 29 }
+54 -3
main.tsx
··· 182 182 ...getPageMeta(galleryLink(handle, rkey)), 183 183 ...getGalleryMeta(gallery), 184 184 ]; 185 - ctx.state.scripts = ["photo_dialog.js", "masonry.js"]; 185 + ctx.state.scripts = ["photo_dialog.js", "masonry.js", "sortable.js"]; 186 186 return ctx.render( 187 187 <GalleryPage favs={favs} gallery={gallery} currentUserDid={did} />, 188 188 ); ··· 571 571 ctx.deleteRecord( 572 572 `at://${ctx.currentUser.did}/social.grain.photo/${params.rkey}`, 573 573 ); 574 + return new Response(null, { status: 200 }); 575 + }), 576 + route("/actions/sort-end", ["POST"], async (req, _params, ctx) => { 577 + const formData = await req.formData(); 578 + const items = formData.getAll("item") as string[]; 579 + console.log(items); 574 580 return new Response(null, { status: 200 }); 575 581 }), 576 582 ...photoUploadRoutes(), ··· 1045 1051 : null} 1046 1052 <script src="https://unpkg.com/htmx.org@1.9.10" /> 1047 1053 <script src="https://unpkg.com/hyperscript.org@0.9.14" /> 1054 + <script src="https://unpkg.com/sortablejs@1.15.6" /> 1048 1055 <style dangerouslySetInnerHTML={{ __html: CSS }} /> 1049 1056 <link rel="stylesheet" href={`/static/styles.css?${cssContentHash}`} /> 1050 1057 <link rel="preconnect" href="https://fonts.googleapis.com" /> ··· 1632 1639 hx-target="#layout" 1633 1640 hx-swap="afterbegin" 1634 1641 > 1642 + Change Sort 1643 + </Button> 1644 + <Button 1645 + variant="primary" 1646 + class="self-start w-full sm:w-fit" 1647 + hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`} 1648 + hx-target="#layout" 1649 + hx-swap="afterbegin" 1650 + > 1635 1651 Edit 1636 1652 </Button> 1637 1653 </div> ··· 1647 1663 ) 1648 1664 : null} 1649 1665 </div> 1650 - <div 1666 + <SortableGrid gallery={gallery} /> 1667 + { 1668 + /* <div 1651 1669 id="masonry-container" 1652 1670 class="h-0 overflow-hidden relative mx-auto w-full" 1653 1671 _="on load or htmx:afterSettle call computeMasonry()" ··· 1665 1683 /> 1666 1684 )) 1667 1685 : null} 1668 - </div> 1686 + </div> */ 1687 + } 1669 1688 </div> 1670 1689 ); 1671 1690 } ··· 1709 1728 ) 1710 1729 : null} 1711 1730 </button> 1731 + ); 1732 + } 1733 + 1734 + function SortableGrid({ 1735 + gallery, 1736 + }: Readonly<{ gallery: GalleryView }>) { 1737 + return ( 1738 + <form 1739 + id="masonry-container" 1740 + class="sortable h-0 overflow-hidden relative mx-auto w-full" 1741 + _="on load or htmx:afterSettle call computeMasonry()" 1742 + // hx-post="/actions/sort-end" 1743 + // hx-trigger="end" 1744 + // hx-swap="none" 1745 + > 1746 + <div class="htmx-indicator">Updating...</div> 1747 + {gallery?.items?.filter(isPhotoView).map((item) => ( 1748 + <div 1749 + key={item.cid} 1750 + class="masonry-tile absolute cursor-pointer" 1751 + data-width={item.aspectRatio?.width} 1752 + data-height={item.aspectRatio?.height} 1753 + > 1754 + <input type="hidden" name="item" value={item.uri} /> 1755 + <img 1756 + src={item.fullsize} 1757 + alt={item.alt} 1758 + class="w-full h-full object-cover" 1759 + /> 1760 + </div> 1761 + ))} 1762 + </form> 1712 1763 ); 1713 1764 } 1714 1765
+28
static/sortable.js
··· 1 + htmx.onLoad(function (content) { 2 + const sortables = content.querySelectorAll(".sortable"); 3 + for (const sortable of sortables) { 4 + const sortableInstance = new Sortable(sortable, { 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 + }); 27 + } 28 + });
+72
static/styles.css
··· 559 559 .lowercase { 560 560 text-transform: lowercase; 561 561 } 562 + .opacity-50 { 563 + opacity: 50%; 564 + } 562 565 .ring-sky-500 { 563 566 --tw-ring-color: var(--color-sky-500); 567 + } 568 + .filter { 569 + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); 564 570 } 565 571 .group-data-\[added\=true\]\:block { 566 572 &:is(:where(.group)[data-added="true"] *) { ··· 715 721 syntax: "*"; 716 722 inherits: false; 717 723 } 724 + @property --tw-blur { 725 + syntax: "*"; 726 + inherits: false; 727 + } 728 + @property --tw-brightness { 729 + syntax: "*"; 730 + inherits: false; 731 + } 732 + @property --tw-contrast { 733 + syntax: "*"; 734 + inherits: false; 735 + } 736 + @property --tw-grayscale { 737 + syntax: "*"; 738 + inherits: false; 739 + } 740 + @property --tw-hue-rotate { 741 + syntax: "*"; 742 + inherits: false; 743 + } 744 + @property --tw-invert { 745 + syntax: "*"; 746 + inherits: false; 747 + } 748 + @property --tw-opacity { 749 + syntax: "*"; 750 + inherits: false; 751 + } 752 + @property --tw-saturate { 753 + syntax: "*"; 754 + inherits: false; 755 + } 756 + @property --tw-sepia { 757 + syntax: "*"; 758 + inherits: false; 759 + } 760 + @property --tw-drop-shadow { 761 + syntax: "*"; 762 + inherits: false; 763 + } 764 + @property --tw-drop-shadow-color { 765 + syntax: "*"; 766 + inherits: false; 767 + } 768 + @property --tw-drop-shadow-alpha { 769 + syntax: "<percentage>"; 770 + inherits: false; 771 + initial-value: 100%; 772 + } 773 + @property --tw-drop-shadow-size { 774 + syntax: "*"; 775 + inherits: false; 776 + } 718 777 @property --tw-shadow { 719 778 syntax: "*"; 720 779 inherits: false; ··· 787 846 --tw-space-x-reverse: 0; 788 847 --tw-border-style: solid; 789 848 --tw-font-weight: initial; 849 + --tw-blur: initial; 850 + --tw-brightness: initial; 851 + --tw-contrast: initial; 852 + --tw-grayscale: initial; 853 + --tw-hue-rotate: initial; 854 + --tw-invert: initial; 855 + --tw-opacity: initial; 856 + --tw-saturate: initial; 857 + --tw-sepia: initial; 858 + --tw-drop-shadow: initial; 859 + --tw-drop-shadow-color: initial; 860 + --tw-drop-shadow-alpha: 100%; 861 + --tw-drop-shadow-size: initial; 790 862 --tw-shadow: 0 0 #0000; 791 863 --tw-shadow-color: initial; 792 864 --tw-shadow-alpha: 100%;