···1+/**
2+ * GENERATED CODE - DO NOT MODIFY
3+ */
4+import { validate as _validate } from '../../../lexicons'
5+import { is$typed as _is$typed } from '../../../util'
6+7+const is$typed = _is$typed,
8+ validate = _validate
9+const id = 'social.drydown.settings'
10+11+export interface Main {
12+ $type: 'social.drydown.settings'
13+ /** How the user prefers to be noticed (1=skin scent, 5=bold presence) */
14+ presenceStyle?: number
15+ /** How important all-day longevity is (1=not important, 5=essential) */
16+ longevityPriority?: number
17+ /** Preference for fragrance complexity (1=simple, 5=intricate) */
18+ complexityPreference?: number
19+ /** How user evaluates fragrances (1=instinct, 5=analytical) */
20+ scoringApproach?: number
21+ /** When viewing others' reviews: show their score or recalculate with your preferences */
22+ scoreLens?: 'theirs' | 'mine' | (string & {})
23+ /** Timestamp when settings were first created */
24+ createdAt: string
25+ /** Timestamp when settings were last updated */
26+ updatedAt?: string
27+ [k: string]: unknown
28+}
29+30+const hashMain = 'main'
31+32+export function isMain<V>(v: V) {
33+ return is$typed(v, id, hashMain)
34+}
35+36+export function validateMain<V>(v: V) {
37+ return validate<Main & V>(v, id, hashMain, true)
38+}
39+40+export {
41+ type Main as Record,
42+ isMain as isRecord,
43+ validateMain as validateRecord,
44+}
+7-6
src/components/EditHousePage.tsx
···1import { useState, useEffect } from 'preact/hooks'
2-import { useLocation, Link } from 'wouter'
03import { AtpBaseClient } from '../client/index'
4import { AppDisclaimer } from './AppDisclaimer'
05import { Footer } from './Footer'
6-import type { OAuthSession } from '@atproto/oauth-client-browser'
7import { deleteHouse } from '../api/houses'
8import { bulkUpdateFragranceHouse } from '../api/fragrances'
9import { Combobox } from './Combobox'
···15 handle: string
16 rkey: string
17 session: OAuthSession | null
0018}
1920-export function EditHousePage({ handle, rkey, session }: EditHousePageProps) {
21 const [, setLocation] = useLocation()
22 const [houseName, setHouseName] = useState('')
23 const [isLoading, setIsLoading] = useState(true)
···216217 return (
218 <div className="page-container">
219- <nav className="main-nav" style={{ marginBottom: '2rem' }}>
220- <Link href="/" className="nav-logo">Drydown</Link>
221- </nav>
222223 <h2>Edit House</h2>
224
···1import { useState, useEffect } from 'preact/hooks'
2import { useLocation } from 'wouter'
3import { SEO } from './SEO'
04import { LoginForm } from './LoginForm'
5import { AppDisclaimer } from './AppDisclaimer'
6import { Footer } from './Footer'
···168 description="Join the Drydown community to create time-based fragrance reviews and discover new perfumes reviewed by real people."
169 url="https://drydown.social"
170 />
00171172 {/* Hero Section */}
173 <section class="landing-hero">
···1import { useState, useEffect } from 'preact/hooks'
2import { useLocation } from 'wouter'
3import { SEO } from './SEO'
4+import { Header } from './Header'
5import { LoginForm } from './LoginForm'
6import { AppDisclaimer } from './AppDisclaimer'
7import { Footer } from './Footer'
···169 description="Join the Drydown community to create time-based fragrance reviews and discover new perfumes reviewed by real people."
170 url="https://drydown.social"
171 />
172+173+ <Header session={null} />
174175 {/* Hero Section */}
176 <section class="landing-hero">
···170 return calculateWeightedScore(review)
171}
17200000000000000000000000000000000000000000000000000000000000000000000000000000000173/**
174 * Format notification time window for a specific stage
175 * Returns formatted text like "in 1.5-4 hours" or specific times
···170 return calculateWeightedScore(review)
171}
172173+export type ScoreLens = 'theirs' | 'mine'
174+175+export interface UserPreferencesForScoring {
176+ presenceStyle?: number
177+ longevityPriority?: number
178+ complexityPreference?: number
179+ scoringApproach?: number
180+ scoreLens?: ScoreLens
181+}
182+183+/**
184+ * Get the display score for a review with optional personalized scoring.
185+ *
186+ * @param review - The review record
187+ * @param userPreferences - Optional user preferences for personalized scoring
188+ * @param isOwnReview - Whether this review belongs to the current user
189+ * @returns Object with score and whether it's personalized
190+ */
191+export function getPersonalizedScore(
192+ review: any,
193+ userPreferences?: UserPreferencesForScoring,
194+ isOwnReview: boolean = false
195+): { score: number; isPersonalized: boolean } {
196+ // If no preferences or viewing own review or lens is "theirs", use original score
197+ if (!userPreferences || isOwnReview || userPreferences.scoreLens !== 'mine') {
198+ return {
199+ score: getReviewDisplayScore(review),
200+ isPersonalized: false,
201+ }
202+ }
203+204+ // Recalculate with user's preferences
205+ const weights = mapPreferencesToWeights(userPreferences)
206+ return {
207+ score: calculateWeightedScore(review, weights),
208+ isPersonalized: true,
209+ }
210+}
211+212+/**
213+ * Maps user preferences to scoring weights.
214+ * This is a simplified version for use in reviewUtils.
215+ */
216+function mapPreferencesToWeights(preferences: UserPreferencesForScoring) {
217+ const WEIGHT_SCALE: Record<number, number> = {
218+ 1: 0.5,
219+ 2: 0.75,
220+ 3: 1.0,
221+ 4: 1.25,
222+ 5: 1.5,
223+ }
224+225+ const SCORING_APPROACH_OVERALL_SCALE: Record<number, number> = {
226+ 1: 1.5,
227+ 2: 1.25,
228+ 3: 1.0,
229+ 4: 0.75,
230+ 5: 0.5,
231+ }
232+233+ const presenceMultiplier = WEIGHT_SCALE[preferences.presenceStyle ?? 3] ?? 1.0
234+ const longevityMultiplier = WEIGHT_SCALE[preferences.longevityPriority ?? 3] ?? 1.0
235+ const complexityMultiplier = WEIGHT_SCALE[preferences.complexityPreference ?? 3] ?? 1.0
236+ const scoringApproach = preferences.scoringApproach ?? 3
237+ const overallMultiplier = SCORING_APPROACH_OVERALL_SCALE[scoringApproach] ?? 1.0
238+ const technicalBoost = scoringApproach >= 4 ? 1.1 : 1.0
239+240+ return {
241+ openingRating: 1.0 * technicalBoost,
242+ openingProjection: presenceMultiplier * technicalBoost,
243+ drydownRating: 1.0 * technicalBoost,
244+ midProjection: presenceMultiplier * technicalBoost,
245+ sillage: presenceMultiplier * technicalBoost,
246+ endRating: longevityMultiplier * technicalBoost,
247+ complexity: complexityMultiplier * technicalBoost,
248+ longevity: longevityMultiplier * technicalBoost,
249+ overallRating: overallMultiplier,
250+ }
251+}
252+253/**
254 * Format notification time window for a specific stage
255 * Returns formatted text like "in 1.5-4 hours" or specific times