From 7e168eb75b1a226150b2d7f7e8016fe89c4182e7 Mon Sep 17 00:00:00 2001 From: miah <144297490+miah120@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:30:08 -0500 Subject: [PATCH] feat: Support User-Level Default Activities (#5125) --- frontend/composables/use-default-activity.ts | 63 +++++++++++++++++++ frontend/composables/use-users/preferences.ts | 19 ++++++ frontend/lang/messages/en-US.json | 4 +- frontend/lib/api/types/activity.ts | 18 ++++++ frontend/pages/index.vue | 11 ++++ frontend/pages/login.vue | 10 +++ frontend/pages/user/profile/edit.vue | 40 +++++++++--- 7 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 frontend/composables/use-default-activity.ts create mode 100644 frontend/lib/api/types/activity.ts diff --git a/frontend/composables/use-default-activity.ts b/frontend/composables/use-default-activity.ts new file mode 100644 index 000000000..ac11bf56a --- /dev/null +++ b/frontend/composables/use-default-activity.ts @@ -0,0 +1,63 @@ +import type { Activity, I18n, TranslationResult } from "~/lib/api/types/activity"; +import { ActivityKey } from "~/lib/api/types/activity"; + +export const DEFAULT_ACTIVITY = "/g/home" as const; + +type ActivityRegistry = { + recipes: Activity; + mealplanner: Activity; + shopping_list: Activity; +}; + +const selectableActivities: ActivityRegistry = { + recipes: { + key: ActivityKey.RECIPES, + route: groupSlug => groupSlug ? `/g/${groupSlug}` : DEFAULT_ACTIVITY, + label: i18n => i18n.t("general.recipes"), + }, + mealplanner: { + key: ActivityKey.MEALPLANNER, + route: () => "/household/mealplan/planner/view", + label: i18n => i18n.t("meal-plan.meal-planner"), + }, + shopping_list: { + key: ActivityKey.SHOPPING_LIST, + route: () => "/shopping-lists", + label: i18n => i18n.t("shopping-list.shopping-lists"), + }, +}; + +function getDefaultActivityRoute(activityKey?: ActivityKey, groupSlug?: string): string { + if (!activityKey) { + return DEFAULT_ACTIVITY; + } + const route = selectableActivities[activityKey]?.route ?? (() => DEFAULT_ACTIVITY); + return route(groupSlug); +} + +function getDefaultActivityLabels(i18n: I18n): TranslationResult[] { + return Object.values(selectableActivities).map( + ({ label }) => label(i18n), + ); +} + +function getActivityKey(i18n: I18n, target: TranslationResult = ""): ActivityKey | undefined { + return Object.values(selectableActivities) + .find(({ label }) => label(i18n) === target)?.key; +} + +function getActivityLabel(i18n: I18n, target?: ActivityKey): TranslationResult { + return Object.values(selectableActivities) + .find(({ key }) => key === target) + ?.label(i18n) ?? ""; +} + +export default function useDefaultActivity() { + return { + selectableActivities, + getDefaultActivityRoute, + getDefaultActivityLabels, + getActivityKey, + getActivityLabel, + }; +} diff --git a/frontend/composables/use-users/preferences.ts b/frontend/composables/use-users/preferences.ts index 336ce7c25..19896eaca 100644 --- a/frontend/composables/use-users/preferences.ts +++ b/frontend/composables/use-users/preferences.ts @@ -1,4 +1,5 @@ import { useLocalStorage, useSessionStorage } from "@vueuse/core"; +import { ActivityKey } from "~/lib/api/types/activity"; import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe"; import type { QueryFilterJSON } from "~/lib/api/types/response"; @@ -65,6 +66,10 @@ export interface UserRecipeCreatePreferences { parseRecipe: boolean; } +export interface UserActivityPreferences { + defaultActivity: ActivityKey; +} + export function useUserMealPlanPreferences(): Ref { const fromStorage = useLocalStorage( "meal-planner-preferences", @@ -115,6 +120,20 @@ export function useUserSortPreferences(): Ref { return fromStorage; } +export function useUserActivityPreferences(): Ref { + const fromStorage = useLocalStorage( + "activity-preferences", + { + defaultActivity: ActivityKey.RECIPES, + }, + { mergeDefaults: true }, + // we cast to a Ref because by default it will return an optional type ref + // but since we pass defaults we know all properties are set. + ) as Ref; + + return fromStorage; +} + export function useUserSearchQuerySession(): Ref { const fromStorage = useSessionStorage( "search-query", diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 0470997c7..4f17e053e 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -1063,7 +1063,9 @@ "dont-want-to-see-this-anymore-be-sure-to-change-your-email": "Don't want to see this anymore? Be sure to change your email in your user settings!", "forgot-password": "Forgot Password", "forgot-password-text": "Please enter your email address and we will send you a link to reset your password.", - "changes-reflected-immediately": "Changes to this user will be reflected immediately." + "changes-reflected-immediately": "Changes to this user will be reflected immediately.", + "default-activity": "Default Activity", + "default-activity-hint": "Select which page you'd like to navigate to upon logging in from this device" }, "language-dialog": { "translated": "translated", diff --git a/frontend/lib/api/types/activity.ts b/frontend/lib/api/types/activity.ts new file mode 100644 index 000000000..9e1ad0049 --- /dev/null +++ b/frontend/lib/api/types/activity.ts @@ -0,0 +1,18 @@ +export type I18n = ReturnType; +export type TRoute = string; +export type TranslationResult = string; + +export type ActivityRoute = (groupSlug?: string) => TRoute; +export type ActivityLabel = (i18n: I18n) => TranslationResult; + +export type Activity = { + key: ActivityKey; + route: ActivityRoute; + label: ActivityLabel; +}; + +export const enum ActivityKey { + RECIPES = "recipes", + MEALPLANNER = "mealplanner", + SHOPPING_LIST = "shopping_list", +} diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index 07619fe4d..1d98128db 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -3,6 +3,8 @@