Onboarding

This commit is contained in:
2026-04-22 21:37:52 +02:00
parent c16fee77af
commit 3e9f863121
21 changed files with 2524 additions and 184 deletions

View File

@@ -31,15 +31,26 @@ export const initDatabase = (): void => {
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS user_settings (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
language TEXT NOT NULL DEFAULT 'de',
language_set INTEGER NOT NULL DEFAULT 0,
appearance_mode TEXT NOT NULL DEFAULT 'system',
color_palette TEXT NOT NULL DEFAULT 'forest',
profile_image TEXT,
onboarding_done INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS user_settings (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
language TEXT NOT NULL DEFAULT 'de',
language_set INTEGER NOT NULL DEFAULT 0,
appearance_mode TEXT NOT NULL DEFAULT 'system',
color_palette TEXT NOT NULL DEFAULT 'forest',
profile_image TEXT,
onboarding_done INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS user_onboarding_profile (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
acquisition_source TEXT,
primary_goal TEXT,
experience_level TEXT,
lexicon_explored INTEGER NOT NULL DEFAULT 0,
customization_done INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS plants (
id TEXT PRIMARY KEY,
@@ -193,7 +204,81 @@ export const SettingsDb = {
// ─── Plants ────────────────────────────────────────────────────────────────────
const DEFAULT_CARE_INFO: CareInfo = {
export const OnboardingProfileDb = {
get(userId: number) {
const db = getDb();
db.runSync('INSERT OR IGNORE INTO user_onboarding_profile (user_id) VALUES (?)', [userId]);
return db.getFirstSync<{
acquisition_source: string | null;
primary_goal: string | null;
experience_level: string | null;
lexicon_explored: number;
customization_done: number;
}>('SELECT * FROM user_onboarding_profile WHERE user_id = ?', [userId])!;
},
setAcquisitionSource(userId: number, source: string) {
const db = getDb();
db.runSync(
`INSERT INTO user_onboarding_profile (user_id, acquisition_source, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET
acquisition_source = excluded.acquisition_source,
updated_at = datetime('now')`,
[userId, source],
);
},
setPrimaryGoal(userId: number, goal: string) {
const db = getDb();
db.runSync(
`INSERT INTO user_onboarding_profile (user_id, primary_goal, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET
primary_goal = excluded.primary_goal,
updated_at = datetime('now')`,
[userId, goal],
);
},
setExperienceLevel(userId: number, level: string) {
const db = getDb();
db.runSync(
`INSERT INTO user_onboarding_profile (user_id, experience_level, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET
experience_level = excluded.experience_level,
updated_at = datetime('now')`,
[userId, level],
);
},
setLexiconExplored(userId: number, explored: boolean) {
const db = getDb();
db.runSync(
`INSERT INTO user_onboarding_profile (user_id, lexicon_explored, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET
lexicon_explored = excluded.lexicon_explored,
updated_at = datetime('now')`,
[userId, explored ? 1 : 0],
);
},
setCustomizationDone(userId: number, done: boolean) {
const db = getDb();
db.runSync(
`INSERT INTO user_onboarding_profile (user_id, customization_done, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET
customization_done = excluded.customization_done,
updated_at = datetime('now')`,
[userId, done ? 1 : 0],
);
},
};
const DEFAULT_CARE_INFO: CareInfo = {
waterIntervalDays: 7,
light: 'Bright indirect light',
temp: '18-25 C',

View File

@@ -0,0 +1,44 @@
import { OnboardingProfileDb } from './database';
export type PersistentOnboardingStep = 'lexicon' | 'customize';
export const OnboardingProgressService = {
isStepCompleted(userId: number, step: PersistentOnboardingStep): boolean {
const profile = OnboardingProfileDb.get(userId);
return step === 'lexicon'
? profile.lexicon_explored === 1
: profile.customization_done === 1;
},
completeStep(userId: number, step: PersistentOnboardingStep): void {
if (step === 'lexicon') {
OnboardingProfileDb.setLexiconExplored(userId, true);
return;
}
OnboardingProfileDb.setCustomizationDone(userId, true);
},
getSignals(userId: number) {
const profile = OnboardingProfileDb.get(userId);
return {
lexiconExplored: profile.lexicon_explored === 1,
customizationDone: profile.customization_done === 1,
};
},
getAcquisitionSource(userId: number): string | null {
return OnboardingProfileDb.get(userId).acquisition_source;
},
setAcquisitionSource(userId: number, source: string): void {
OnboardingProfileDb.setAcquisitionSource(userId, source);
},
setPrimaryGoal(userId: number, goal: string): void {
OnboardingProfileDb.setPrimaryGoal(userId, goal);
},
setExperienceLevel(userId: number, level: string): void {
OnboardingProfileDb.setExperienceLevel(userId, level);
},
};