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

Configure Feed

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

Update layout

Expands navbar fullscreen for a more modern look. Same layout on mobile where the navbar flows with the scroll (not fixed).

![screenshot](https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreihssk6un6npe4apd6lxcwcik6a5gi6p2cp4fhjhaxcsoupt6lsfiu@jpeg)

+115 -76
+3 -4
src/app.tsx
··· 12 12 undefined; 13 13 const staticFilesHash = props.ctx.state.staticFilesHash; 14 14 return ( 15 - <html lang="en" class="w-full h-full"> 15 + <html lang="en" class="h-full"> 16 16 <head> 17 17 <meta charset="UTF-8" /> 18 18 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> ··· 56 56 /> 57 57 ))} 58 58 </head> 59 - <body class="h-full w-full dark:bg-zinc-950 dark:text-white"> 60 - <Layout id="layout" class="border-zinc-200 dark:border-zinc-800"> 59 + <body class="h-full dark:bg-zinc-950 dark:text-white"> 60 + <Layout id="layout"> 61 61 <Layout.Nav 62 62 heading={ 63 63 <h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white"> ··· 67 67 } 68 68 profile={profile} 69 69 hasNotifications={hasNotifications} 70 - class="border-zinc-200 dark:border-zinc-800" 71 70 /> 72 71 <Layout.Content>{props.children}</Layout.Content> 73 72 </Layout>
+1 -1
src/components/AvatarDialog.tsx
··· 7 7 profile, 8 8 }: Readonly<{ profile: Un$Typed<ProfileView> }>) { 9 9 return ( 10 - <Dialog> 10 + <Dialog class="z-100"> 11 11 <Dialog.X /> 12 12 <div 13 13 class="w-[400px] h-[400px] flex flex-col p-4 z-10"
+1 -1
src/components/GalleryCreateEditDialog.tsx
··· 6 6 gallery, 7 7 }: Readonly<{ gallery?: GalleryView | null }>) { 8 8 return ( 9 - <Dialog id="gallery-dialog" class="z-30"> 9 + <Dialog id="gallery-dialog" class="z-100"> 10 10 <Dialog.Content class="dark:bg-zinc-950 relative"> 11 11 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 12 12 <Dialog.Title>
+1 -1
src/components/GallerySortDialog.tsx
··· 7 7 { gallery }: Readonly<{ gallery: GalleryView }>, 8 8 ) { 9 9 return ( 10 - <Dialog> 10 + <Dialog class="z-100"> 11 11 <Dialog.Content class="dark:bg-zinc-950 relative"> 12 12 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 13 13 <Dialog.Title>Sort gallery</Dialog.Title>
+60 -55
src/components/Layout.tsx
··· 25 25 return ( 26 26 <div 27 27 class={cn( 28 - "h-full max-w-5xl mx-auto sm:border-x relative", 28 + "min-h-screen flex flex-col max-w-5xl mx-auto sm:border-x border-zinc-200 dark:border-zinc-800", 29 29 classProp, 30 30 )} 31 31 {...props} ··· 40 40 ) => { 41 41 return ( 42 42 <main 43 - class={cn("h-[calc(100vh-56px)] sm:overflow-y-auto", classProp)} 43 + class={cn( 44 + "flex-1 sm:pt-14", // pt-14 pushes content below the fixed nav 45 + classProp, 46 + )} 44 47 {...props} 45 48 > 46 49 {children} ··· 60 63 return ( 61 64 <nav 62 65 class={cn( 63 - "w-full border-b border-zinc-950 flex justify-between items-center px-4 h-14", 66 + "sm:fixed sm:top-0 sm:left-0 sm:right-0 h-14 z-50 bg-white dark:bg-zinc-950 border-b border-zinc-200 dark:border-zinc-800", 64 67 classProp, 65 68 )} 66 69 {...props} 67 70 > 68 - <div class="flex items-center space-x-4"> 69 - <a href="/"> 70 - {heading} 71 - </a> 72 - </div> 73 - <div class="space-x-2"> 74 - {profile 75 - ? ( 76 - <div class="flex items-center ts:space-x-1 sm:space-x-2"> 77 - <form hx-post="/logout" hx-swap="none" class="inline"> 78 - <Button type="submit" variant="secondary">Sign out</Button> 79 - </form> 80 - <Button 81 - asChild 82 - variant="secondary" 83 - class="relative pl-2" 84 - > 85 - <a href="/explore"> 86 - <i class="fas fa-search text-zinc-950 dark:text-zinc-50" /> 71 + <div class="mx-auto max-w-5xl h-full flex items-center justify-between px-4"> 72 + <div class="flex items-center space-x-4"> 73 + <a href="/"> 74 + {heading} 75 + </a> 76 + </div> 77 + <div class="flex space-x-2"> 78 + {profile 79 + ? ( 80 + <div class="flex items-center ts:space-x-1 sm:space-x-2"> 81 + <form hx-post="/logout" hx-swap="none" class="inline"> 82 + <Button type="submit" variant="secondary">Sign out</Button> 83 + </form> 84 + <Button 85 + asChild 86 + variant="secondary" 87 + class="relative pl-2" 88 + > 89 + <a href="/explore"> 90 + <i class="fas fa-search text-zinc-950 dark:text-zinc-50" /> 91 + </a> 92 + </Button> 93 + <Button 94 + asChild 95 + variant="secondary" 96 + class="relative pl-2" 97 + > 98 + <a href="/notifications"> 99 + <i class="fas fa-bell text-zinc-950 dark:text-zinc-50" /> 100 + {hasNotifications 101 + ? ( 102 + <span class="absolute inline-flex items-center justify-center w-3 h-3 text-xs font-bold text-white bg-sky-500 rounded-full top-1 right-1" /> 103 + ) 104 + : null} 105 + </a> 106 + </Button> 107 + <a href={`/profile/${profile.handle}`}> 108 + <ActorAvatar profile={profile} size={32} /> 87 109 </a> 88 - </Button> 89 - <Button 90 - asChild 91 - variant="secondary" 92 - class="relative pl-2" 93 - > 94 - <a href="/notifications"> 95 - <i class="fas fa-bell text-zinc-950 dark:text-zinc-50" /> 96 - {hasNotifications 97 - ? ( 98 - <span class="absolute inline-flex items-center justify-center w-3 h-3 text-xs font-bold text-white bg-sky-500 rounded-full top-1 right-1" /> 99 - ) 100 - : null} 101 - </a> 102 - </Button> 103 - <a href={`/profile/${profile.handle}`}> 104 - <ActorAvatar profile={profile} size={32} /> 105 - </a> 106 - </div> 107 - ) 108 - : ( 109 - <div class="flex items-center space-x-4"> 110 - <form hx-post="/signup" hx-swap="none" class="inline"> 111 - <Button variant="secondary" type="submit"> 112 - Create account 110 + </div> 111 + ) 112 + : ( 113 + <div class="flex items-center space-x-4"> 114 + <form hx-post="/signup" hx-swap="none" class="inline"> 115 + <Button variant="secondary" type="submit"> 116 + Create account 117 + </Button> 118 + </form> 119 + <Button variant="secondary" asChild> 120 + <a href="/login"> 121 + Sign in 122 + </a> 113 123 </Button> 114 - </form> 115 - <Button variant="secondary" asChild> 116 - <a href="/login"> 117 - Sign in 118 - </a> 119 - </Button> 120 - </div> 121 - )} 124 + </div> 125 + )} 126 + </div> 122 127 </div> 123 128 </nav> 124 129 );
+1 -1
src/components/LoginPage.tsx
··· 5 5 return ( 6 6 <div 7 7 id="login" 8 - class="flex justify-center items-center w-full h-full relative" 8 + class="flex justify-center items-center w-full h-[calc(100vh-56px)] relative" 9 9 style="background-image: url('https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg'); background-size: cover; background-position: center;" 10 10 > 11 11 <Login hx-target="#login" error={error} errorClass="text-white" />
+1 -1
src/components/PhotoAltDialog.tsx
··· 8 8 photo: PhotoView; 9 9 }>) { 10 10 return ( 11 - <Dialog id="photo-alt-dialog" class="z-30"> 11 + <Dialog id="photo-alt-dialog" class="z-100"> 12 12 <Dialog.Content class="dark:bg-zinc-950 relative"> 13 13 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 14 14 <Dialog.Title>Add alt text</Dialog.Title>
+1 -1
src/components/PhotoDialog.tsx
··· 15 15 prevImage?: PhotoView; 16 16 }>) { 17 17 return ( 18 - <Dialog id="photo-dialog" class="bg-zinc-950 z-30"> 18 + <Dialog id="photo-dialog" class="bg-zinc-950 z-100"> 19 19 <Dialog.X /> 20 20 {nextImage 21 21 ? (
+1 -1
src/components/PhotoSelectDialog.tsx
··· 13 13 photos: PhotoView[]; 14 14 }>) { 15 15 return ( 16 - <Dialog id="photo-select-dialog" class="z-30"> 16 + <Dialog id="photo-select-dialog" class="z-100"> 17 17 <Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col relative"> 18 18 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 19 19 <Dialog.Title>Add photos</Dialog.Title>
+1 -1
src/components/ProfileDialog.tsx
··· 8 8 profile: ProfileView; 9 9 }>) { 10 10 return ( 11 - <Dialog> 11 + <Dialog class="z-100"> 12 12 <Dialog.Content class="dark:bg-zinc-950 relative"> 13 13 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 14 14 <Dialog.Title>Edit my profile</Dialog.Title>
+1 -1
src/routes/explore.tsx
··· 53 53 <div class="my-4"> 54 54 <Input 55 55 name="q" 56 - class="dark:bg-zinc-800 dark:text-white" 56 + class="dark:bg-zinc-800 dark:text-white border-zinc-100 bg-zinc-100 dark:border-zinc-800" 57 57 placeholder="Search for users" 58 58 hx-get="/explore" 59 59 hx-target="#search-results"
+43 -8
static/styles.css
··· 198 198 .absolute { 199 199 position: absolute; 200 200 } 201 + .fixed { 202 + position: fixed; 203 + } 201 204 .relative { 202 205 position: relative; 203 206 } ··· 248 251 } 249 252 .z-30 { 250 253 z-index: 30; 254 + } 255 + .z-50 { 256 + z-index: 50; 257 + } 258 + .z-100 { 259 + z-index: 100; 251 260 } 252 261 .container { 253 262 width: 100%; ··· 354 363 } 355 364 .h-full { 356 365 height: 100%; 366 + } 367 + .min-h-screen { 368 + min-height: 100vh; 357 369 } 358 370 .w-1\/3 { 359 371 width: calc(1/3 * 100%); ··· 536 548 .border-zinc-900 { 537 549 border-color: var(--color-zinc-900); 538 550 } 539 - .border-zinc-950 { 540 - border-color: var(--color-zinc-950); 541 - } 542 551 .bg-black { 543 552 background-color: var(--color-black); 544 553 } ··· 550 559 } 551 560 .bg-sky-500 { 552 561 background-color: var(--color-sky-500); 562 + } 563 + .bg-white { 564 + background-color: var(--color-white); 553 565 } 554 566 .bg-zinc-100 { 555 567 background-color: var(--color-zinc-100); ··· 595 607 } 596 608 .pt-4 { 597 609 padding-top: calc(var(--spacing) * 4); 610 + } 611 + .pt-14 { 612 + padding-top: calc(var(--spacing) * 14); 598 613 } 599 614 .pb-4 { 600 615 padding-bottom: calc(var(--spacing) * 4); ··· 710 725 opacity: 50%; 711 726 } 712 727 } 728 + .sm\:fixed { 729 + @media (width >= 40rem) { 730 + position: fixed; 731 + } 732 + } 733 + .sm\:top-0 { 734 + @media (width >= 40rem) { 735 + top: calc(var(--spacing) * 0); 736 + } 737 + } 738 + .sm\:right-0 { 739 + @media (width >= 40rem) { 740 + right: calc(var(--spacing) * 0); 741 + } 742 + } 713 743 .sm\:right-1 { 714 744 @media (width >= 40rem) { 715 745 right: calc(var(--spacing) * 1); ··· 718 748 .sm\:bottom-1 { 719 749 @media (width >= 40rem) { 720 750 bottom: calc(var(--spacing) * 1); 751 + } 752 + } 753 + .sm\:left-0 { 754 + @media (width >= 40rem) { 755 + left: calc(var(--spacing) * 0); 721 756 } 722 757 } 723 758 .sm\:h-screen { ··· 789 824 } 790 825 } 791 826 } 792 - .sm\:overflow-y-auto { 793 - @media (width >= 40rem) { 794 - overflow-y: auto; 795 - } 796 - } 797 827 .sm\:border-x { 798 828 @media (width >= 40rem) { 799 829 border-inline-style: var(--tw-border-style); ··· 803 833 .sm\:px-0 { 804 834 @media (width >= 40rem) { 805 835 padding-inline: calc(var(--spacing) * 0); 836 + } 837 + } 838 + .sm\:pt-14 { 839 + @media (width >= 40rem) { 840 + padding-top: calc(var(--spacing) * 14); 806 841 } 807 842 } 808 843 .dark\:divide-zinc-800 {