a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere. drydown.social
at oauth-update 247 lines 6.0 kB view raw view rendered
1# Data Models 2 3**Related Documents**: 4- [Lexicon Schemas](./lexicon-schemas.md) - AT Protocol schemas 5- [Storage Strategy](./storage-strategy.md) - Where data is stored 6- [Requirements](../product/requirements.md) - Validation rules 7 8## Overview 9 10TypeScript interfaces and types that map to AT Protocol lexicons and application state. 11 12**Status**: 🔴 Planned 13 14--- 15 16## Core Types 17 18### FragranceReview 19 20Maps to `social.drydown.review` lexicon: 21 22```typescript 23interface FragranceReview { 24 // Metadata (not in AT Protocol record) 25 id?: string // Local ID for in-progress reviews 26 uri?: string // AT Protocol record URI: at://did/collection/rkey 27 28 // Fragrance info 29 fragrance: { 30 name: string // 1-200 chars, required 31 brand?: string // 0-100 chars, optional 32 year?: number // 1000-2100, optional 33 } 34 35 // Timestamps (ISO 8601 format) 36 createdAt: string // When Stage 1 started 37 stage2CompletedAt?: string // When Stage 2 completed 38 stage3CompletedAt?: string // When Stage 3 completed 39 completedAt?: string // When all stages done 40 41 // Ratings 42 stage1: Stage1Ratings 43 stage2?: Stage2Ratings 44 stage3?: Stage3Ratings 45 46 // Review content 47 textReview?: string // Max 275 graphemes 48 calculatedRating?: number // 0-5, to 3 decimal places 49 50 // Sharing 51 sharedToBluesky: boolean 52 sharedAt?: string // ISO 8601, when last shared 53} 54``` 55 56### Stage Ratings 57 58```typescript 59interface Stage1Ratings { 60 openingAppeal: number // 1-5, integer 61 projection: number // 1-5, integer 62 firstImpression: number // 1-5, integer 63} 64 65interface Stage2Ratings { 66 heartAppeal: number // 1-5, integer 67 heartProjection: number // 1-5, integer 68 skipped: boolean 69} 70 71interface Stage3Ratings { 72 stillDetectable: number // 1-5, integer (longevity) 73 sillage: number // 1-5, integer 74 skinChemistry: number // 1-5, integer 75 complexity: number // 1-5, integer 76 gutScore: number // 1-5, integer 77} 78``` 79 80### UserSettings 81 82Maps to `social.drydown.settings` lexicon: 83 84```typescript 85interface UserSettings { 86 // Metadata 87 createdAt: string // ISO 8601 88 updatedAt: string // ISO 8601 89 90 // Rating weights 91 ratingWeights: RatingWeights 92 93 // App preferences 94 blueskyClient: 'default' | 'blacksky' | 'graysky' 95} 96 97interface RatingWeights { 98 openingAppeal: number // 0-10, default 1.0 99 projection: number // 0-10, default 1.0 100 firstImpression: number // 0-10, default 1.0 101 heartAppeal: number // 0-10, default 1.0 102 heartProjection: number // 0-10, default 1.0 103 stillDetectable: number // 0-10, default 1.0 104 sillage: number // 0-10, default 1.0 105 skinChemistry: number // 0-10, default 1.0 106 complexity: number // 0-10, default 1.0 107 gutScore: number // 0-10, default 1.0 108} 109 110const DEFAULT_WEIGHTS: RatingWeights = { 111 openingAppeal: 1.0, 112 projection: 1.0, 113 firstImpression: 1.0, 114 heartAppeal: 1.0, 115 heartProjection: 1.0, 116 stillDetectable: 1.0, 117 sillage: 1.0, 118 skinChemistry: 1.0, 119 complexity: 1.0, 120 gutScore: 1.0 121} 122``` 123 124--- 125 126## Utility Functions 127 128### Calculate Final Rating 129 130```typescript 131function calculateFinalRating( 132 review: FragranceReview, 133 weights: RatingWeights 134): number { 135 let totalWeightedScore = 0 136 let totalWeights = 0 137 138 // Stage 1 (always present) 139 if (review.stage1) { 140 totalWeightedScore += review.stage1.openingAppeal * weights.openingAppeal 141 totalWeights += weights.openingAppeal 142 143 totalWeightedScore += review.stage1.projection * weights.projection 144 totalWeights += weights.projection 145 146 totalWeightedScore += review.stage1.firstImpression * weights.firstImpression 147 totalWeights += weights.firstImpression 148 } 149 150 // Stage 2 (if not skipped) 151 if (review.stage2 && !review.stage2.skipped) { 152 totalWeightedScore += review.stage2.heartAppeal * weights.heartAppeal 153 totalWeights += weights.heartAppeal 154 155 totalWeightedScore += review.stage2.heartProjection * weights.heartProjection 156 totalWeights += weights.heartProjection 157 } 158 159 // Stage 3 (always present if review completed) 160 if (review.stage3) { 161 totalWeightedScore += review.stage3.stillDetectable * weights.stillDetectable 162 totalWeights += weights.stillDetectable 163 164 totalWeightedScore += review.stage3.sillage * weights.sillage 165 totalWeights += weights.sillage 166 167 totalWeightedScore += review.stage3.skinChemistry * weights.skinChemistry 168 totalWeights += weights.skinChemistry 169 170 totalWeightedScore += review.stage3.complexity * weights.complexity 171 totalWeights += weights.complexity 172 173 totalWeightedScore += review.stage3.gutScore * weights.gutScore 174 totalWeights += weights.gutScore 175 } 176 177 // Handle edge case: all weights are 0 178 if (totalWeights === 0) { 179 return 0 180 } 181 182 const rating = totalWeightedScore / totalWeights 183 184 // Round to 3 decimal places 185 return Math.round(rating * 1000) / 1000 186} 187``` 188 189### Format Rating for Display 190 191```typescript 192function formatRatingPrecise(rating: number): string { 193 return `${rating.toFixed(3)} / 5.000` 194} 195 196function formatRatingStars(rating: number): string { 197 const rounded = Math.round(rating) 198 const filled = '★'.repeat(rounded) 199 const empty = '☆'.repeat(5 - rounded) 200 return filled + empty 201} 202``` 203 204### Count Graphemes 205 206```typescript 207function countGraphemes(text: string): number { 208 const segmenter = new Intl.Segmenter() 209 return Array.from(segmenter.segment(text)).length 210} 211 212function validateTextLength(text: string, maxGraphemes: number): boolean { 213 return countGraphemes(text) <= maxGraphemes 214} 215``` 216 217--- 218 219## State Management Types 220 221### App State 222 223```typescript 224interface AppState { 225 // Auth 226 session: OAuthSession | null 227 isInitializing: boolean 228 229 // Reviews 230 inProgressReviews: FragranceReview[] 231 completedReviews: FragranceReview[] 232 233 // Settings 234 userSettings: UserSettings 235 236 // UI 237 loading: boolean 238 error: string | null 239} 240``` 241 242--- 243 244**Related Documents**: 245- [Lexicon Schemas](./lexicon-schemas.md) - AT Protocol record definitions 246- [Storage Strategy](./storage-strategy.md) - Data persistence 247- [Requirements](../product/requirements.md) - Validation rules