WIP PWA for Grain
0
fork

Configure Feed

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

docs: add settings page implementation plan

+321
+321
docs/plans/2025-12-25-settings-page.md
··· 1 + # Settings Page Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add a settings page with sign out, accessible via gear icon on user's own profile. 6 + 7 + **Architecture:** Gear icon on profile header (own profile only) navigates to `/settings`. Settings page has back button and sign out row. 8 + 9 + **Tech Stack:** Lit 3.x, existing auth service, grain-icon component 10 + 11 + --- 12 + 13 + ### Task 1: Add Icons to grain-icon.js 14 + 15 + **Files:** 16 + - Modify: `src/components/atoms/grain-icon.js` 17 + 18 + **Step 1: Add gear and logout icons to ICONS object** 19 + 20 + Add after line 13 (after `userFilled`): 21 + 22 + ```javascript 23 + gear: 'fa-solid fa-gear', 24 + logout: 'fa-solid fa-right-from-bracket' 25 + ``` 26 + 27 + **Step 2: Commit** 28 + 29 + ```bash 30 + git add src/components/atoms/grain-icon.js 31 + git commit -m "feat: add gear and logout icons" 32 + ``` 33 + 34 + --- 35 + 36 + ### Task 2: Add Settings Icon to Profile Header 37 + 38 + **Files:** 39 + - Modify: `src/components/organisms/grain-profile-header.js` 40 + 41 + **Step 1: Add imports** 42 + 43 + Add after line 1: 44 + 45 + ```javascript 46 + import { router } from '../../router.js'; 47 + import { auth } from '../../services/auth.js'; 48 + import '../atoms/grain-icon.js'; 49 + ``` 50 + 51 + **Step 2: Add _user state property** 52 + 53 + Update static properties to: 54 + 55 + ```javascript 56 + static properties = { 57 + profile: { type: Object }, 58 + _showFullscreen: { state: true }, 59 + _user: { state: true } 60 + }; 61 + ``` 62 + 63 + **Step 3: Update constructor** 64 + 65 + ```javascript 66 + constructor() { 67 + super(); 68 + this._showFullscreen = false; 69 + this._user = auth.user; 70 + } 71 + ``` 72 + 73 + **Step 4: Add connectedCallback and disconnectedCallback** 74 + 75 + Add after constructor: 76 + 77 + ```javascript 78 + connectedCallback() { 79 + super.connectedCallback(); 80 + this._unsubscribe = auth.subscribe(user => { 81 + this._user = user; 82 + }); 83 + } 84 + 85 + disconnectedCallback() { 86 + super.disconnectedCallback(); 87 + this._unsubscribe?.(); 88 + } 89 + ``` 90 + 91 + **Step 5: Add isOwnProfile getter and goToSettings method** 92 + 93 + Add after disconnectedCallback: 94 + 95 + ```javascript 96 + get #isOwnProfile() { 97 + return this._user?.handle && this._user.handle === this.profile?.handle; 98 + } 99 + 100 + #goToSettings() { 101 + router.push('/settings'); 102 + } 103 + ``` 104 + 105 + **Step 6: Add settings button styles** 106 + 107 + Add to static styles after `.fullscreen-overlay img`: 108 + 109 + ```css 110 + .settings-button { 111 + background: none; 112 + border: none; 113 + padding: 0; 114 + cursor: pointer; 115 + color: var(--color-text-secondary); 116 + margin-left: auto; 117 + } 118 + ``` 119 + 120 + **Step 7: Update top-row in render** 121 + 122 + Replace the top-row div with: 123 + 124 + ```javascript 125 + <div class="top-row"> 126 + <button class="avatar-button" @click=${this.#openFullscreen}> 127 + <grain-avatar 128 + src=${avatarUrl || ''} 129 + alt=${handle || ''} 130 + size="lg" 131 + ></grain-avatar> 132 + </button> 133 + <grain-profile-stats 134 + galleryCount=${galleryCount || 0} 135 + followerCount=${followerCount || 0} 136 + followingCount=${followingCount || 0} 137 + ></grain-profile-stats> 138 + ${this.#isOwnProfile ? html` 139 + <button class="settings-button" @click=${this.#goToSettings}> 140 + <grain-icon name="gear" size="20"></grain-icon> 141 + </button> 142 + ` : ''} 143 + </div> 144 + ``` 145 + 146 + **Step 8: Commit** 147 + 148 + ```bash 149 + git add src/components/organisms/grain-profile-header.js 150 + git commit -m "feat: add settings icon to own profile header" 151 + ``` 152 + 153 + --- 154 + 155 + ### Task 3: Create Settings Page 156 + 157 + **Files:** 158 + - Create: `src/components/pages/grain-settings.js` 159 + 160 + **Step 1: Create the settings page component** 161 + 162 + ```javascript 163 + import { LitElement, html, css } from 'lit'; 164 + import { router } from '../../router.js'; 165 + import { auth } from '../../services/auth.js'; 166 + import '../atoms/grain-icon.js'; 167 + 168 + export class GrainSettings extends LitElement { 169 + static styles = css` 170 + :host { 171 + display: block; 172 + min-height: 100vh; 173 + min-height: 100dvh; 174 + padding-bottom: 80px; 175 + } 176 + .header { 177 + display: flex; 178 + align-items: center; 179 + gap: var(--space-sm); 180 + padding: var(--space-md) var(--space-sm); 181 + } 182 + @media (min-width: 600px) { 183 + .header { 184 + padding-left: 0; 185 + padding-right: 0; 186 + } 187 + } 188 + .back-button { 189 + background: none; 190 + border: none; 191 + padding: 8px; 192 + cursor: pointer; 193 + color: var(--color-text-primary); 194 + margin-left: -8px; 195 + } 196 + h1 { 197 + font-size: var(--font-size-md); 198 + font-weight: var(--font-weight-semibold); 199 + color: var(--color-text-primary); 200 + margin: 0; 201 + } 202 + .settings-list { 203 + border-top: 1px solid var(--color-border); 204 + } 205 + .settings-row { 206 + display: flex; 207 + align-items: center; 208 + gap: var(--space-sm); 209 + padding: var(--space-md) var(--space-sm); 210 + background: none; 211 + border: none; 212 + border-bottom: 1px solid var(--color-border); 213 + width: 100%; 214 + cursor: pointer; 215 + color: var(--color-text-primary); 216 + font-size: var(--font-size-sm); 217 + text-align: left; 218 + } 219 + @media (min-width: 600px) { 220 + .settings-row { 221 + padding-left: 0; 222 + padding-right: 0; 223 + } 224 + } 225 + .settings-row:active { 226 + background: var(--color-bg-elevated); 227 + } 228 + .settings-row.danger { 229 + color: #ff4444; 230 + } 231 + `; 232 + 233 + #goBack() { 234 + history.back(); 235 + } 236 + 237 + #signOut() { 238 + auth.logout(); 239 + router.push('/'); 240 + } 241 + 242 + render() { 243 + return html` 244 + <div class="header"> 245 + <button class="back-button" @click=${this.#goBack}> 246 + <grain-icon name="back" size="20"></grain-icon> 247 + </button> 248 + <h1>Settings</h1> 249 + </div> 250 + <div class="settings-list"> 251 + <button class="settings-row danger" @click=${this.#signOut}> 252 + <grain-icon name="logout" size="18"></grain-icon> 253 + Sign Out 254 + </button> 255 + </div> 256 + `; 257 + } 258 + } 259 + 260 + customElements.define('grain-settings', GrainSettings); 261 + ``` 262 + 263 + **Step 2: Commit** 264 + 265 + ```bash 266 + git add src/components/pages/grain-settings.js 267 + git commit -m "feat: add settings page with sign out" 268 + ``` 269 + 270 + --- 271 + 272 + ### Task 4: Register Settings Route 273 + 274 + **Files:** 275 + - Modify: `src/components/pages/grain-app.js` 276 + 277 + **Step 1: Import settings page** 278 + 279 + Add after line 7 (after grain-gallery-detail import): 280 + 281 + ```javascript 282 + import './grain-settings.js'; 283 + ``` 284 + 285 + **Step 2: Register route** 286 + 287 + Add after line 31 (after gallery-detail route, before wildcard): 288 + 289 + ```javascript 290 + .register('/settings', 'grain-settings') 291 + ``` 292 + 293 + **Step 3: Commit** 294 + 295 + ```bash 296 + git add src/components/pages/grain-app.js 297 + git commit -m "feat: register settings route" 298 + ``` 299 + 300 + --- 301 + 302 + ### Task 5: Test Settings Flow 303 + 304 + **Step 1: Manual testing checklist** 305 + 306 + 1. Log in to the app 307 + 2. Navigate to your own profile via bottom nav 308 + 3. Verify gear icon appears in profile header 309 + 4. Click gear icon, verify navigation to /settings 310 + 5. Verify back button returns to profile 311 + 6. Click Sign Out, verify redirect to home 312 + 7. Verify header shows Login button (logged out) 313 + 8. Navigate to another user's profile 314 + 9. Verify gear icon does NOT appear 315 + 316 + **Step 2: Commit any fixes if needed** 317 + 318 + ```bash 319 + git add -A 320 + git commit -m "fix: settings flow polish" 321 + ```