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.

feat: improve signed-out experience with prominent sign-in

- Desktop sidebar: show logo, tagline, and sign-in button when signed out
- Mobile bottom bar: show tagline and sign-in button instead of nav tabs
- Mobile drawer: move sign-in button to top of drawer
- Hide home icon and following feed tab when signed out
- Widen sidebar column for signed-out text content
- Simplify AuthBar to logout-only (sign-in moved to sidebar)
- Update login placeholder to user.bsky.social

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+109 -55
+4 -1
app/lib/components/molecules/FeedTabs.svelte
··· 1 1 <script lang="ts"> 2 2 import { page } from '$app/state' 3 3 import { pinnedFeeds } from '$lib/preferences' 4 + import { isAuthenticated } from '$lib/stores' 4 5 5 - const tabFeeds = $derived($pinnedFeeds) 6 + const tabFeeds = $derived( 7 + $isAuthenticated ? $pinnedFeeds : $pinnedFeeds.filter((f) => f.id !== 'following') 8 + ) 6 9 </script> 7 10 8 11 <div class="center-header">
+39 -23
app/lib/components/molecules/MobileBottomBar.svelte
··· 2 2 import { Image, Search, Plus, Bell } from 'lucide-svelte' 3 3 import { goto } from '$app/navigation' 4 4 import { page } from '$app/state' 5 - import { isAuthenticated, viewer } from '$lib/stores' 5 + import { isAuthenticated, loginModalOpen, viewer } from '$lib/stores' 6 6 import Avatar from '../atoms/Avatar.svelte' 7 + import Button from '../atoms/Button.svelte' 7 8 import { createQuery } from '@tanstack/svelte-query' 8 9 import { unseenNotificationCountQuery } from '$lib/queries' 9 10 ··· 15 16 })) 16 17 </script> 17 18 18 - <div class="mobile-bottom"> 19 - <button 20 - class="mobile-tab" 21 - class:active={page.url.pathname === '/'} 22 - onclick={() => goto('/')} 23 - > 24 - <Image size={22} /> 25 - </button> 26 - {#if $isAuthenticated} 19 + {#if $isAuthenticated} 20 + <div class="mobile-bottom"> 21 + <button 22 + class="mobile-tab" 23 + class:active={page.url.pathname === '/'} 24 + onclick={() => goto('/')} 25 + > 26 + <Image size={22} /> 27 + </button> 27 28 <button 28 29 class="mobile-tab" 29 30 class:active={page.url.pathname === '/create'} ··· 43 44 {/if} 44 45 </span> 45 46 </button> 46 - {/if} 47 - <button class="mobile-tab" onclick={onSearch}> 48 - <Search size={22} /> 49 - </button> 50 - {#if $isAuthenticated && $viewer} 51 - <button 52 - class="mobile-tab" 53 - class:active={page.url.pathname.startsWith('/profile/')} 54 - onclick={() => goto(`/profile/${encodeURIComponent($viewer!.did)}`)} 55 - > 56 - <Avatar did={$viewer.did} src={$viewer.avatar} name={$viewer.displayName || $viewer.handle} size={24} /> 47 + <button class="mobile-tab" onclick={onSearch}> 48 + <Search size={22} /> 57 49 </button> 58 - {/if} 59 - </div> 50 + {#if $viewer} 51 + <button 52 + class="mobile-tab" 53 + class:active={page.url.pathname.startsWith('/profile/')} 54 + onclick={() => goto(`/profile/${encodeURIComponent($viewer!.did)}`)} 55 + > 56 + <Avatar did={$viewer.did} src={$viewer.avatar} name={$viewer.displayName || $viewer.handle} size={24} /> 57 + </button> 58 + {/if} 59 + </div> 60 + {:else} 61 + <div class="mobile-bottom mobile-bottom-signed-out"> 62 + <span class="mobile-tagline">Share your photography</span> 63 + <Button size="sm" onclick={() => ($loginModalOpen = true)}>Sign in</Button> 64 + </div> 65 + {/if} 60 66 61 67 <style> 62 68 .mobile-bottom { ··· 110 116 justify-content: center; 111 117 padding: 0 3px; 112 118 font-family: var(--font-body); 119 + } 120 + 121 + .mobile-bottom-signed-out { 122 + justify-content: space-between; 123 + padding: 0 16px env(safe-area-inset-bottom, 0px); 124 + } 125 + .mobile-tagline { 126 + font-size: 14px; 127 + font-weight: 600; 128 + color: var(--text-secondary); 113 129 } 114 130 115 131 @media (max-width: 600px) {
+5 -16
app/lib/components/organisms/AuthBar.svelte
··· 1 1 <script lang="ts"> 2 - import { LogIn, LogOut } from 'lucide-svelte' 3 - import LoginModal from './LoginModal.svelte' 4 - import { viewer, isAuthenticated } from '$lib/stores' 2 + import { LogOut } from 'lucide-svelte' 3 + import { viewer } from '$lib/stores' 5 4 import { logout } from '$lib/auth' 6 5 import { resetPreferences } from '$lib/preferences' 7 6 8 - let loginOpen = $state(false) 9 - 10 7 async function doLogout() { 11 8 await logout() 12 9 resetPreferences() ··· 15 12 } 16 13 </script> 17 14 18 - <LoginModal bind:open={loginOpen} /> 19 - 20 15 <div class="auth-bar"> 21 - {#if $isAuthenticated && $viewer} 22 - <button class="nav-item" title="Sign out" onclick={doLogout}> 23 - <LogOut size={20} /> 24 - </button> 25 - {:else} 26 - <button class="nav-item" title="Sign in" onclick={() => (loginOpen = true)}> 27 - <LogIn size={20} /> 28 - </button> 29 - {/if} 16 + <button class="nav-item" title="Sign out" onclick={doLogout}> 17 + <LogOut size={20} /> 18 + </button> 30 19 </div> 31 20 32 21 <style>
+1 -1
app/lib/components/organisms/LoginModal.svelte
··· 94 94 <div class="input-wrapper"> 95 95 <input 96 96 type="text" 97 - placeholder="e.g. jasmine.garden" 97 + placeholder="e.g. user.bsky.social" 98 98 bind:value={handle} 99 99 bind:this={inputEl} 100 100 oninput={onInput}
+14 -2
app/lib/components/organisms/MobileDrawer.svelte
··· 39 39 <button type="button" class="drawer-logo" onclick={() => nav('/')}>grain</button> 40 40 </div> 41 41 42 + {#if !$isAuthenticated} 43 + <div class="drawer-sign-in"> 44 + <Button onclick={() => { open = false; loginOpen = true }}>Sign In</Button> 45 + </div> 46 + {/if} 47 + 42 48 {#each $pinnedFeeds as feed (feed.id)} 43 49 {@const Icon = feedIcon(feed)} 44 50 <button class="drawer-link" onclick={() => nav(feed.path)}> ··· 87 93 <span class="drawer-handle">{$viewer.handle || $viewer.displayName}</span> 88 94 </div> 89 95 <Button variant="secondary" onclick={() => { open = false; doLogout() }}>Sign Out</Button> 90 - {:else} 91 - <Button onclick={() => { open = false; loginOpen = true }}>Sign In</Button> 92 96 {/if} 93 97 </div> 94 98 </div> ··· 198 202 .camera-pill:hover { 199 203 border-color: var(--grain); 200 204 color: var(--text-primary); 205 + } 206 + .drawer-sign-in { 207 + padding: 0 8px 12px; 208 + border-bottom: 1px solid var(--border); 209 + margin-bottom: 8px; 210 + } 211 + .drawer-sign-in :global(.btn) { 212 + width: 100%; 201 213 } 202 214 .drawer-auth { 203 215 margin-top: auto;
+42 -10
app/lib/components/organisms/Sidebar.svelte
··· 2 2 import { Home, Plus, Settings, Bell } from 'lucide-svelte' 3 3 import AuthBar from './AuthBar.svelte' 4 4 import Avatar from '../atoms/Avatar.svelte' 5 + import Button from '../atoms/Button.svelte' 6 + import LoginModal from './LoginModal.svelte' 5 7 import { isAuthenticated, viewer } from '$lib/stores' 6 8 import { page } from '$app/state' 7 9 import { createQuery } from '@tanstack/svelte-query' 8 10 import { unseenNotificationCountQuery } from '$lib/queries' 11 + 12 + let loginOpen = $state(false) 9 13 10 14 const unseenCount = createQuery(() => ({ 11 15 ...unseenNotificationCountQuery($viewer?.did ?? ''), ··· 13 17 })) 14 18 </script> 15 19 16 - <nav class="sidebar-left"> 20 + {#if !$isAuthenticated} 21 + <LoginModal bind:open={loginOpen} /> 22 + {/if} 23 + 24 + <nav class="sidebar-left" class:signed-out={!$isAuthenticated}> 17 25 <div class="sidebar-top"> 18 26 {#if $isAuthenticated && $viewer} 19 27 <a href="/profile/{$viewer.did}" class="sidebar-avatar-link"> 20 28 <Avatar did={$viewer.did} src={$viewer.avatar} name={$viewer.displayName || $viewer.handle} size={42} /> 21 29 </a> 22 30 {:else} 23 - <a href="/" class="logo-text">grain</a> 31 + <div class="signed-out-hero"> 32 + <a href="/" class="logo-text">grain</a> 33 + <p class="sidebar-tagline">Share your<br/>photography</p> 34 + <Button size="sm" onclick={() => (loginOpen = true)}>Sign in</Button> 35 + </div> 24 36 {/if} 25 37 </div> 26 38 <div class="nav-items"> 27 - <a href="/" class="nav-item" class:active={page.url.pathname === '/'} title="Home"> 28 - <Home size={22} /> 29 - </a> 30 39 {#if $isAuthenticated} 40 + <a href="/" class="nav-item" class:active={page.url.pathname === '/'} title="Home"> 41 + <Home size={22} /> 42 + </a> 31 43 <a href="/notifications" class="nav-item" class:active={page.url.pathname === '/notifications'} title="Notifications"> 32 44 <span class="bell-wrap"> 33 45 <Bell size={22} /> ··· 44 56 </a> 45 57 {/if} 46 58 </div> 47 - <div class="sidebar-bottom"> 48 - <AuthBar /> 49 - </div> 59 + {#if $isAuthenticated} 60 + <div class="sidebar-bottom"> 61 + <AuthBar /> 62 + </div> 63 + {/if} 50 64 </nav> 51 65 52 66 <style> ··· 61 75 border-right: 1px solid var(--border); 62 76 z-index: 101; 63 77 } 78 + .signed-out { 79 + align-items: flex-start; 80 + padding: 20px 16px 16px; 81 + } 64 82 .sidebar-top { 65 83 margin-bottom: 28px; 66 84 } 85 + .signed-out-hero { 86 + display: flex; 87 + flex-direction: column; 88 + align-items: flex-start; 89 + gap: 10px; 90 + } 67 91 .logo-text { 68 92 font-family: var(--font-display); 69 93 font-weight: 800; 70 - font-size: 15px; 94 + font-size: 22px; 71 95 display: block; 72 - text-align: center; 73 96 color: #fff; 74 97 text-decoration: none; 98 + letter-spacing: -0.02em; 99 + } 100 + .sidebar-tagline { 101 + font-size: 14px; 102 + font-weight: 600; 103 + color: var(--text-muted); 104 + margin: 0; 105 + line-height: 1.35; 106 + letter-spacing: -0.01em; 75 107 } 76 108 .sidebar-avatar-link { text-decoration: none; } 77 109 .nav-items {
+4 -2
app/lib/components/templates/Shell.svelte
··· 7 7 import MobileDrawer from '../organisms/MobileDrawer.svelte' 8 8 import MobileSearch from '../organisms/MobileSearch.svelte' 9 9 import LoginModal from '../organisms/LoginModal.svelte' 10 - import { loginModalOpen } from '$lib/stores' 10 + import { loginModalOpen, isAuthenticated } from '$lib/stores' 11 + 12 + const colLeft = $derived($isAuthenticated ? '78px' : '140px') 11 13 12 14 let { children }: { children: Snippet } = $props() 13 15 let drawerOpen = $state(false) ··· 16 18 17 19 <MobileTopBar onHamburger={() => drawerOpen = true} onSearch={() => searchOpen = true} /> 18 20 19 - <div class="shell"> 21 + <div class="shell" style:--col-left={colLeft}> 20 22 <Sidebar /> 21 23 <main class="col-center"> 22 24 {@render children()}