a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere.
drydown.social
Data Models#
Related Documents:
- Lexicon Schemas - AT Protocol schemas
- Storage Strategy - Where data is stored
- Requirements - Validation rules
Overview#
TypeScript interfaces and types that map to AT Protocol lexicons and application state.
Status: 🔴 Planned
Core Types#
FragranceReview#
Maps to social.drydown.review lexicon:
interface FragranceReview {
// Metadata (not in AT Protocol record)
id?: string // Local ID for in-progress reviews
uri?: string // AT Protocol record URI: at://did/collection/rkey
// Fragrance info
fragrance: {
name: string // 1-200 chars, required
brand?: string // 0-100 chars, optional
year?: number // 1000-2100, optional
}
// Timestamps (ISO 8601 format)
createdAt: string // When Stage 1 started
stage2CompletedAt?: string // When Stage 2 completed
stage3CompletedAt?: string // When Stage 3 completed
completedAt?: string // When all stages done
// Ratings
stage1: Stage1Ratings
stage2?: Stage2Ratings
stage3?: Stage3Ratings
// Review content
textReview?: string // Max 275 graphemes
calculatedRating?: number // 0-5, to 3 decimal places
// Sharing
sharedToBluesky: boolean
sharedAt?: string // ISO 8601, when last shared
}
Stage Ratings#
interface Stage1Ratings {
openingAppeal: number // 1-5, integer
projection: number // 1-5, integer
firstImpression: number // 1-5, integer
}
interface Stage2Ratings {
heartAppeal: number // 1-5, integer
heartProjection: number // 1-5, integer
skipped: boolean
}
interface Stage3Ratings {
stillDetectable: number // 1-5, integer (longevity)
sillage: number // 1-5, integer
skinChemistry: number // 1-5, integer
complexity: number // 1-5, integer
gutScore: number // 1-5, integer
}
UserSettings#
Maps to social.drydown.settings lexicon:
interface UserSettings {
// Metadata
createdAt: string // ISO 8601
updatedAt: string // ISO 8601
// Rating weights
ratingWeights: RatingWeights
// App preferences
blueskyClient: 'default' | 'blacksky' | 'graysky'
}
interface RatingWeights {
openingAppeal: number // 0-10, default 1.0
projection: number // 0-10, default 1.0
firstImpression: number // 0-10, default 1.0
heartAppeal: number // 0-10, default 1.0
heartProjection: number // 0-10, default 1.0
stillDetectable: number // 0-10, default 1.0
sillage: number // 0-10, default 1.0
skinChemistry: number // 0-10, default 1.0
complexity: number // 0-10, default 1.0
gutScore: number // 0-10, default 1.0
}
const DEFAULT_WEIGHTS: RatingWeights = {
openingAppeal: 1.0,
projection: 1.0,
firstImpression: 1.0,
heartAppeal: 1.0,
heartProjection: 1.0,
stillDetectable: 1.0,
sillage: 1.0,
skinChemistry: 1.0,
complexity: 1.0,
gutScore: 1.0
}
Utility Functions#
Calculate Final Rating#
function calculateFinalRating(
review: FragranceReview,
weights: RatingWeights
): number {
let totalWeightedScore = 0
let totalWeights = 0
// Stage 1 (always present)
if (review.stage1) {
totalWeightedScore += review.stage1.openingAppeal * weights.openingAppeal
totalWeights += weights.openingAppeal
totalWeightedScore += review.stage1.projection * weights.projection
totalWeights += weights.projection
totalWeightedScore += review.stage1.firstImpression * weights.firstImpression
totalWeights += weights.firstImpression
}
// Stage 2 (if not skipped)
if (review.stage2 && !review.stage2.skipped) {
totalWeightedScore += review.stage2.heartAppeal * weights.heartAppeal
totalWeights += weights.heartAppeal
totalWeightedScore += review.stage2.heartProjection * weights.heartProjection
totalWeights += weights.heartProjection
}
// Stage 3 (always present if review completed)
if (review.stage3) {
totalWeightedScore += review.stage3.stillDetectable * weights.stillDetectable
totalWeights += weights.stillDetectable
totalWeightedScore += review.stage3.sillage * weights.sillage
totalWeights += weights.sillage
totalWeightedScore += review.stage3.skinChemistry * weights.skinChemistry
totalWeights += weights.skinChemistry
totalWeightedScore += review.stage3.complexity * weights.complexity
totalWeights += weights.complexity
totalWeightedScore += review.stage3.gutScore * weights.gutScore
totalWeights += weights.gutScore
}
// Handle edge case: all weights are 0
if (totalWeights === 0) {
return 0
}
const rating = totalWeightedScore / totalWeights
// Round to 3 decimal places
return Math.round(rating * 1000) / 1000
}
Format Rating for Display#
function formatRatingPrecise(rating: number): string {
return `${rating.toFixed(3)} / 5.000`
}
function formatRatingStars(rating: number): string {
const rounded = Math.round(rating)
const filled = '★'.repeat(rounded)
const empty = '☆'.repeat(5 - rounded)
return filled + empty
}
Count Graphemes#
function countGraphemes(text: string): number {
const segmenter = new Intl.Segmenter()
return Array.from(segmenter.segment(text)).length
}
function validateTextLength(text: string, maxGraphemes: number): boolean {
return countGraphemes(text) <= maxGraphemes
}
State Management Types#
App State#
interface AppState {
// Auth
session: OAuthSession | null
isInitializing: boolean
// Reviews
inProgressReviews: FragranceReview[]
completedReviews: FragranceReview[]
// Settings
userSettings: UserSettings
// UI
loading: boolean
error: string | null
}
Related Documents:
- Lexicon Schemas - AT Protocol record definitions
- Storage Strategy - Data persistence
- Requirements - Validation rules