fixed profile preferences
This commit is contained in:
@@ -13,21 +13,25 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import {
|
||||||
|
avatarVibeOptions,
|
||||||
|
focusTopicOptions,
|
||||||
|
saveOnboardingPreferences,
|
||||||
|
} from "@/lib/profile-preferences-storage";
|
||||||
import { routes } from "@/lib/routes";
|
import { routes } from "@/lib/routes";
|
||||||
|
|
||||||
const questions = [
|
const questions = [
|
||||||
{
|
{
|
||||||
id: "focus",
|
id: "focus",
|
||||||
prompt: "What habit do you most want to build first?",
|
prompt: "What habit do you most want to build first?",
|
||||||
options: ["Sleep", "Movement", "Hydration", "Mindfulness"],
|
options: focusTopicOptions,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pet-name",
|
id: "avatarVibe",
|
||||||
prompt: "Pick a starter vibe for your pet.",
|
prompt: "Pick a starter vibe for your pet.",
|
||||||
options: ["Calm", "Energetic", "Curious", "Cozy"],
|
options: avatarVibeOptions,
|
||||||
},
|
},
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export default function OnboardingQuizPage() {
|
export default function OnboardingQuizPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -60,18 +64,7 @@ export default function OnboardingQuizPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const supabase = createClient();
|
await saveOnboardingPreferences(answers.focus, answers.avatarVibe);
|
||||||
const { error: updateError } = await supabase.auth.updateUser({
|
|
||||||
data: {
|
|
||||||
onboarding_completed: true,
|
|
||||||
onboarding_answers: answers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updateError) {
|
|
||||||
throw updateError;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push(routes.avatar);
|
router.push(routes.avatar);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
const [dailyTasks, setDailyTasks] = useState<DailyTask[]>([]);
|
const [dailyTasks, setDailyTasks] = useState<DailyTask[]>([]);
|
||||||
const [customHabits, setCustomHabits] = useState<Habit[]>([]);
|
const [customHabits, setCustomHabits] = useState<Habit[]>([]);
|
||||||
const [newHabitLabel, setNewHabitLabel] = useState("");
|
const [newHabitLabel, setNewHabitLabel] = useState("");
|
||||||
const [focusTopics, setFocusTopics] = useState<string[]>([]);
|
const [focusTopic, setFocusTopic] = useState<string | null>(null);
|
||||||
const [quizCompletedToday, setQuizCompletedToday] = useState(false);
|
const [quizCompletedToday, setQuizCompletedToday] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
@@ -218,7 +218,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
]);
|
]);
|
||||||
setDailyTasks(tasks);
|
setDailyTasks(tasks);
|
||||||
setCustomHabits(customs);
|
setCustomHabits(customs);
|
||||||
setFocusTopics(getProfilePreferences().focusTopics);
|
setFocusTopic((await getProfilePreferences()).focusTopic);
|
||||||
setQuizCompletedToday(await hasCompletedDailyQuizToday());
|
setQuizCompletedToday(await hasCompletedDailyQuizToday());
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
@@ -389,7 +389,7 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
|
|||||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
Focus topics:{" "}
|
Focus topics:{" "}
|
||||||
{focusTopics.length > 0 ? focusTopics.join(", ") : "None selected"}
|
{focusTopic ?? "None selected"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Daily quiz today:{" "}
|
Daily quiz today:{" "}
|
||||||
|
|||||||
@@ -33,24 +33,46 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
|||||||
defaultProfilePreferences,
|
defaultProfilePreferences,
|
||||||
);
|
);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreferences(getProfilePreferences());
|
async function loadPreferences() {
|
||||||
|
try {
|
||||||
|
setPreferences(await getProfilePreferences());
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(
|
||||||
|
loadError instanceof Error
|
||||||
|
? loadError.message
|
||||||
|
: "Could not load profile preferences.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadPreferences();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updatePreferences = (updates: Partial<ProfilePreferences>) => {
|
const updatePreferences = async (updates: Partial<ProfilePreferences>) => {
|
||||||
const nextPreferences = { ...preferences, ...updates };
|
const nextPreferences = { ...preferences, ...updates };
|
||||||
setPreferences(nextPreferences);
|
setPreferences(nextPreferences);
|
||||||
saveProfilePreferences(nextPreferences);
|
setIsSaving(true);
|
||||||
};
|
setError(null);
|
||||||
|
|
||||||
const toggleFocusTopic = (topic: string) => {
|
try {
|
||||||
const focusTopics = preferences.focusTopics.includes(topic)
|
const savedPreferences = await saveProfilePreferences(nextPreferences);
|
||||||
? preferences.focusTopics.filter((item) => item !== topic)
|
setPreferences(savedPreferences);
|
||||||
: [...preferences.focusTopics, topic];
|
} catch (saveError) {
|
||||||
|
setPreferences(preferences);
|
||||||
updatePreferences({ focusTopics });
|
setError(
|
||||||
|
saveError instanceof Error
|
||||||
|
? saveError.message
|
||||||
|
: "Could not save profile preferences.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
@@ -61,6 +83,8 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Profile information</CardTitle>
|
<CardTitle className="text-base">Profile information</CardTitle>
|
||||||
@@ -76,21 +100,23 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Focus topic preferences</CardTitle>
|
<CardTitle className="text-base">Focus topic preference</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Choose the areas you want your pet and daily tasks to focus on.
|
Choose the one area you want your pet and daily tasks to focus on.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-2">
|
||||||
{focusTopicOptions.map((topic) => (
|
{focusTopicOptions.map((topic) => (
|
||||||
<div key={topic} className="flex items-center gap-3 rounded-lg border p-3">
|
<Button
|
||||||
<Checkbox
|
key={topic}
|
||||||
id={`focus-${topic}`}
|
type="button"
|
||||||
checked={preferences.focusTopics.includes(topic)}
|
variant={preferences.focusTopic === topic ? "default" : "outline"}
|
||||||
onCheckedChange={() => toggleFocusTopic(topic)}
|
className="w-full justify-start"
|
||||||
/>
|
disabled={isSaving}
|
||||||
<Label htmlFor={`focus-${topic}`}>{topic}</Label>
|
onClick={() => void updatePreferences({ focusTopic: topic })}
|
||||||
</div>
|
>
|
||||||
|
{topic}
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -106,8 +132,9 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
|||||||
key={vibe}
|
key={vibe}
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={isSaving}
|
||||||
variant={preferences.avatarVibe === vibe ? "default" : "outline"}
|
variant={preferences.avatarVibe === vibe ? "default" : "outline"}
|
||||||
onClick={() => updatePreferences({ avatarVibe: vibe })}
|
onClick={() => void updatePreferences({ avatarVibe: vibe })}
|
||||||
>
|
>
|
||||||
{vibe}
|
{vibe}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -127,9 +154,10 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
|||||||
<Label htmlFor="daily-reminder">Daily reminder</Label>
|
<Label htmlFor="daily-reminder">Daily reminder</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="daily-reminder"
|
id="daily-reminder"
|
||||||
|
disabled={isSaving}
|
||||||
checked={preferences.dailyReminderEnabled}
|
checked={preferences.dailyReminderEnabled}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updatePreferences({ dailyReminderEnabled: checked === true })
|
void updatePreferences({ dailyReminderEnabled: checked === true })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,10 +166,10 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
|||||||
<Input
|
<Input
|
||||||
id="reminder-time"
|
id="reminder-time"
|
||||||
type="time"
|
type="time"
|
||||||
|
disabled={isSaving || !preferences.dailyReminderEnabled}
|
||||||
value={preferences.dailyReminderTime}
|
value={preferences.dailyReminderTime}
|
||||||
disabled={!preferences.dailyReminderEnabled}
|
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
updatePreferences({ dailyReminderTime: event.target.value })
|
void updatePreferences({ dailyReminderTime: event.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function getCatalogTaskReason(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getDailyTasks(): Promise<DailyTask[]> {
|
export async function getDailyTasks(): Promise<DailyTask[]> {
|
||||||
const preferences = getProfilePreferences();
|
const preferences = await getProfilePreferences();
|
||||||
const quizAnswers = await getQuizAnswersForToday();
|
const quizAnswers = await getQuizAnswersForToday();
|
||||||
|
|
||||||
const selectedCatalogIds = habitCatalog
|
const selectedCatalogIds = habitCatalog
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ export function matchesFocusTopics(
|
|||||||
entry: HabitCatalogEntry,
|
entry: HabitCatalogEntry,
|
||||||
preferences: ProfilePreferences,
|
preferences: ProfilePreferences,
|
||||||
) {
|
) {
|
||||||
return entry.focusTopics.some((topic) =>
|
return entry.focusTopics.includes(preferences.focusTopic);
|
||||||
preferences.focusTopics.includes(topic),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchesQuizAnswers(
|
export function matchesQuizAnswers(
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
||||||
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
|
||||||
export type ProfilePreferences = {
|
export type ProfilePreferences = {
|
||||||
focusTopics: string[];
|
focusTopic: string;
|
||||||
avatarVibe: string;
|
avatarVibe: string;
|
||||||
dailyReminderEnabled: boolean;
|
dailyReminderEnabled: boolean;
|
||||||
dailyReminderTime: string;
|
dailyReminderTime: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = "habit-pet-profile-preferences";
|
type StoredProfilePreferences = {
|
||||||
|
focusTopic?: string;
|
||||||
|
avatarVibe?: string;
|
||||||
|
dailyReminderEnabled?: boolean;
|
||||||
|
dailyReminderTime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProfileRow = {
|
||||||
|
focus_topics: string | string[] | null;
|
||||||
|
onboarding_complete: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
export const focusTopicOptions = [
|
export const focusTopicOptions = [
|
||||||
"Sleep",
|
"Sleep",
|
||||||
@@ -19,34 +30,301 @@ export const focusTopicOptions = [
|
|||||||
export const avatarVibeOptions = ["Calm", "Energetic", "Curious", "Cozy"] as const;
|
export const avatarVibeOptions = ["Calm", "Energetic", "Curious", "Cozy"] as const;
|
||||||
|
|
||||||
export const defaultProfilePreferences: ProfilePreferences = {
|
export const defaultProfilePreferences: ProfilePreferences = {
|
||||||
focusTopics: ["Movement"],
|
focusTopic: "Movement",
|
||||||
avatarVibe: "Calm",
|
avatarVibe: "Calm",
|
||||||
dailyReminderEnabled: false,
|
dailyReminderEnabled: false,
|
||||||
dailyReminderTime: "20:00",
|
dailyReminderTime: "20:00",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getProfilePreferences(): ProfilePreferences {
|
function isFocusTopic(value: string) {
|
||||||
if (typeof window === "undefined") {
|
return focusTopicOptions.includes(value as (typeof focusTopicOptions)[number]);
|
||||||
return defaultProfilePreferences;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
function isAvatarVibe(value: string) {
|
||||||
|
return avatarVibeOptions.includes(value as (typeof avatarVibeOptions)[number]);
|
||||||
if (!raw) {
|
|
||||||
return defaultProfilePreferences;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
function normalizeFocusTopic(value: string | undefined) {
|
||||||
|
if (value && isFocusTopic(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultProfilePreferences.focusTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAvatarVibe(value: string | undefined) {
|
||||||
|
if (value && isAvatarVibe(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultProfilePreferences.avatarVibe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePreferences(
|
||||||
|
partial: Partial<ProfilePreferences>,
|
||||||
|
): ProfilePreferences {
|
||||||
return {
|
return {
|
||||||
...defaultProfilePreferences,
|
focusTopic: normalizeFocusTopic(partial.focusTopic),
|
||||||
...(JSON.parse(raw) as Partial<ProfilePreferences>),
|
avatarVibe: normalizeAvatarVibe(partial.avatarVibe),
|
||||||
|
dailyReminderEnabled:
|
||||||
|
partial.dailyReminderEnabled ??
|
||||||
|
defaultProfilePreferences.dailyReminderEnabled,
|
||||||
|
dailyReminderTime:
|
||||||
|
partial.dailyReminderTime ?? defaultProfilePreferences.dailyReminderTime,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStoredPreferencesJson(raw: string): Partial<ProfilePreferences> {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as StoredProfilePreferences;
|
||||||
|
|
||||||
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return defaultProfilePreferences;
|
return { focusTopic: raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { focusTopic: raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFocusTopicsField(
|
||||||
|
value: string | string[] | null | undefined,
|
||||||
|
): Partial<ProfilePreferences> {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseStoredPreferencesJson(value[0] ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value?.trim()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseStoredPreferencesJson(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeFocusTopicsField(preferences: ProfilePreferences) {
|
||||||
|
const payload: StoredProfilePreferences = {
|
||||||
|
focusTopic: preferences.focusTopic,
|
||||||
|
avatarVibe: preferences.avatarVibe,
|
||||||
|
dailyReminderEnabled: preferences.dailyReminderEnabled,
|
||||||
|
dailyReminderTime: preferences.dailyReminderTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFocusTopicsTypeError(message: string) {
|
||||||
|
return /array|malformed|invalid input syntax/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRowToPreferences(row: ProfileRow): ProfilePreferences {
|
||||||
|
return normalizePreferences(parseFocusTopicsField(row.focus_topics));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasStoredPreferences(row: ProfileRow) {
|
||||||
|
if (Array.isArray(row.focus_topics)) {
|
||||||
|
return row.focus_topics.some((value) => value.trim().length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(row.focus_topics?.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthenticatedUserId() {
|
||||||
|
const supabase = createClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error,
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (error || !user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOnboardingAnswersFromMetadata(
|
||||||
|
metadata: Record<string, unknown> | undefined,
|
||||||
|
) {
|
||||||
|
const answers = metadata?.onboarding_answers;
|
||||||
|
|
||||||
|
if (!answers || typeof answers !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = answers as Record<string, string>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
focusTopic: record.focus,
|
||||||
|
avatarVibe: record.avatarVibe ?? record["pet-name"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProfileRow(
|
||||||
|
userId: string,
|
||||||
|
preferences: ProfilePreferences,
|
||||||
|
options?: { markOnboardingComplete?: boolean },
|
||||||
|
) {
|
||||||
|
const supabase = createClient();
|
||||||
|
const serialized = serializeFocusTopicsField(preferences);
|
||||||
|
const focusTopicAttempts: Array<string | string[]> = [
|
||||||
|
serialized,
|
||||||
|
[serialized],
|
||||||
|
];
|
||||||
|
const onboardingFields = options?.markOnboardingComplete
|
||||||
|
? { onboarding_complete: true }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
let lastError: string | null = null;
|
||||||
|
|
||||||
|
for (const focus_topics of focusTopicAttempts) {
|
||||||
|
const { data: updatedRows, error: updateError } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.update({
|
||||||
|
focus_topics,
|
||||||
|
...onboardingFields,
|
||||||
|
})
|
||||||
|
.eq("id", userId)
|
||||||
|
.select("focus_topics, onboarding_complete");
|
||||||
|
|
||||||
|
if (!updateError && updatedRows && updatedRows.length > 0) {
|
||||||
|
return updatedRows[0] as ProfileRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
lastError = updateError.message;
|
||||||
|
|
||||||
|
if (!isFocusTopicsTypeError(updateError.message)) {
|
||||||
|
throw new Error(updateError.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveProfilePreferences(preferences: ProfilePreferences) {
|
for (const focus_topics of focusTopicAttempts) {
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
const { data: insertedRow, error: insertError } = await supabase
|
||||||
notifyHabitPetDataUpdated();
|
.from("profiles")
|
||||||
|
.insert({
|
||||||
|
id: userId,
|
||||||
|
focus_topics,
|
||||||
|
...onboardingFields,
|
||||||
|
})
|
||||||
|
.select("focus_topics, onboarding_complete")
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!insertError && insertedRow) {
|
||||||
|
return insertedRow as ProfileRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
lastError = insertError.message;
|
||||||
|
|
||||||
|
if (!isFocusTopicsTypeError(insertError.message)) {
|
||||||
|
throw new Error(insertError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(lastError ?? "Could not save profile preferences.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPreferencesFromMetadata(userId: string) {
|
||||||
|
const supabase = createClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
const onboardingAnswers = getOnboardingAnswersFromMetadata(user?.user_metadata);
|
||||||
|
|
||||||
|
if (!onboardingAnswers?.focusTopic && !onboardingAnswers?.avatarVibe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = normalizePreferences(onboardingAnswers);
|
||||||
|
const row = await saveProfileRow(userId, preferences, {
|
||||||
|
markOnboardingComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapRowToPreferences(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProfilePreferences(): Promise<ProfilePreferences> {
|
||||||
|
const userId = await getAuthenticatedUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return defaultProfilePreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.select("focus_topics, onboarding_complete")
|
||||||
|
.eq("id", userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return defaultProfilePreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = data as ProfileRow;
|
||||||
|
|
||||||
|
if (!hasStoredPreferences(row)) {
|
||||||
|
const synced = await syncPreferencesFromMetadata(userId);
|
||||||
|
return synced ?? mapRowToPreferences(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapRowToPreferences(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveProfilePreferences(preferences: ProfilePreferences) {
|
||||||
|
const userId = await getAuthenticatedUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("You must be signed in to save profile preferences.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPreferences = normalizePreferences(preferences);
|
||||||
|
await saveProfileRow(userId, normalizedPreferences);
|
||||||
|
notifyHabitPetDataUpdated();
|
||||||
|
return normalizedPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveOnboardingPreferences(
|
||||||
|
focusTopic: string,
|
||||||
|
avatarVibe: string,
|
||||||
|
) {
|
||||||
|
const userId = await getAuthenticatedUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("You must be signed in to save onboarding preferences.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
const preferences = normalizePreferences({ focusTopic, avatarVibe });
|
||||||
|
|
||||||
|
await saveProfileRow(userId, preferences, { markOnboardingComplete: true });
|
||||||
|
|
||||||
|
const { error: authError } = await supabase.auth.updateUser({
|
||||||
|
data: {
|
||||||
|
onboarding_completed: true,
|
||||||
|
onboarding_answers: {
|
||||||
|
focus: preferences.focusTopic,
|
||||||
|
avatarVibe: preferences.avatarVibe,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authError) {
|
||||||
|
throw new Error(authError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyHabitPetDataUpdated();
|
||||||
|
return preferences;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- Store profile preferences as JSON text instead of text[].
|
||||||
|
-- Safe when the column is already text (no-op cast).
|
||||||
|
|
||||||
|
alter table public.profiles
|
||||||
|
alter column focus_topics drop default;
|
||||||
|
|
||||||
|
alter table public.profiles
|
||||||
|
alter column focus_topics type text using (
|
||||||
|
case
|
||||||
|
when focus_topics is null then null
|
||||||
|
when pg_typeof(focus_topics)::text = 'text[]' then
|
||||||
|
case
|
||||||
|
when coalesce(array_length(focus_topics::text[], 1), 0) = 0 then null
|
||||||
|
else (focus_topics::text[])[1]
|
||||||
|
end
|
||||||
|
else focus_topics::text
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.profiles
|
||||||
|
alter column focus_topics set default null;
|
||||||
Reference in New Issue
Block a user