a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere.
drydown.social
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