mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			579 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			579 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <v-container
 | |
|     fluid
 | |
|     class="d-flex justify-center  align-start  fill-height"
 | |
|     :class="{
 | |
|       'bg-off-white': !$vuetify.theme.current.dark && !isDark,
 | |
|     }"
 | |
|   >
 | |
|     <!-- Header Toolbar -->
 | |
|     <v-card class="elevation-4" width="1200" :class="{ 'my-10': $vuetify.display.mdAndUp }">
 | |
|       <v-toolbar
 | |
|         color="primary"
 | |
|         class="d-flex justify-center"
 | |
|         dark
 | |
|       >
 | |
|         <v-toolbar-title class="headline text-h4 text-center mx-0">
 | |
|           Mealie
 | |
|         </v-toolbar-title>
 | |
|       </v-toolbar>
 | |
| 
 | |
|       <!-- Stepper Wizard -->
 | |
|       <v-stepper v-model="currentPage" mobile-breakpoint="sm">
 | |
|         <v-stepper-header>
 | |
|           <v-stepper-item
 | |
|             :value="Pages.LANDING"
 | |
|             :complete="currentPage > Pages.LANDING"
 | |
|             :title="$t('general.start')"
 | |
|           />
 | |
|           <v-divider />
 | |
|           <v-stepper-item
 | |
|             :value="Pages.USER_INFO"
 | |
|             :complete="currentPage > Pages.USER_INFO"
 | |
|             :title="$t('user-registration.account-details')"
 | |
|           />
 | |
|           <v-divider />
 | |
|           <v-stepper-item
 | |
|             :value="Pages.PAGE_2"
 | |
|             :complete="currentPage > Pages.PAGE_2"
 | |
|             :title="$t('settings.site-settings')"
 | |
|           />
 | |
|           <v-divider />
 | |
|           <v-stepper-item
 | |
|             :value="Pages.CONFIRM"
 | |
|             :complete="currentPage > Pages.CONFIRM"
 | |
|             :title="$t('admin.maintenance.summary-title')"
 | |
|           />
 | |
|           <v-divider />
 | |
|           <v-stepper-item
 | |
|             :value="Pages.END"
 | |
|             :complete="currentPage > Pages.END"
 | |
|             :title="$t('admin.setup.setup-complete')"
 | |
|           />
 | |
|         </v-stepper-header>
 | |
|         <v-progress-linear
 | |
|           v-if="isSubmitting && currentPage === Pages.CONFIRM"
 | |
|           color="primary"
 | |
|           indeterminate
 | |
|           class="mb-2"
 | |
|         />
 | |
| 
 | |
|         <v-stepper-window :transition="false" class="stepper-window">
 | |
|           <!-- LANDING -->
 | |
|           <v-stepper-window-item :value="Pages.LANDING">
 | |
|             <v-container class="mb-12">
 | |
|               <AppLogo />
 | |
|               <v-card-title class="text-h4 justify-center text-center text-break text-pre-wrap">
 | |
|                 {{ $t('admin.setup.welcome-to-mealie-get-started') }}
 | |
|               </v-card-title>
 | |
|               <v-btn
 | |
|                 :to="groupSlug ? `/g/${groupSlug}` : '/login'"
 | |
|                 rounded
 | |
|                 variant="outlined"
 | |
|                 color="grey-lighten-1"
 | |
|                 class="text-subtitle-2 d-flex mx-auto"
 | |
|                 style="width: fit-content;"
 | |
|               >
 | |
|                 {{ $t('admin.setup.already-set-up-bring-to-homepage') }}
 | |
|               </v-btn>
 | |
|             </v-container>
 | |
| 
 | |
|             <v-card-actions class="justify-center flex-column py-8">
 | |
|               <BaseButton
 | |
|                 size="large"
 | |
|                 color="primary"
 | |
|                 :icon="$globals.icons.translate"
 | |
|                 @click="langDialog = true"
 | |
|               >
 | |
|                 {{ $t('language-dialog.choose-language') }}
 | |
|               </BaseButton>
 | |
|             </v-card-actions>
 | |
| 
 | |
|             <v-stepper-actions
 | |
|               class="justify-end"
 | |
|               :disabled="isSubmitting"
 | |
|               next-text="general.next"
 | |
|               @click:next="onNext"
 | |
|             >
 | |
|               <template #prev />
 | |
|             </v-stepper-actions>
 | |
|           </v-stepper-window-item>
 | |
| 
 | |
|           <!-- USER INFO -->
 | |
|           <v-stepper-window-item :value="Pages.USER_INFO" eager>
 | |
|             <v-container max-width="880">
 | |
|               <UserRegistrationForm />
 | |
|             </v-container>
 | |
|             <v-stepper-actions
 | |
|               :disabled="isSubmitting"
 | |
|               prev-text="general.back"
 | |
|               next-text="general.next"
 | |
|               @click:prev="onPrev"
 | |
|               @click:next="onNext"
 | |
|             />
 | |
|           </v-stepper-window-item>
 | |
| 
 | |
|           <!-- COMMON SETTINGS -->
 | |
|           <v-stepper-window-item :value="Pages.PAGE_2">
 | |
|             <v-container max-width="880">
 | |
|               <v-card-title class="headline pa-0">
 | |
|                 {{ $t('admin.setup.common-settings-for-new-sites') }}
 | |
|               </v-card-title>
 | |
|               <AutoForm
 | |
|                 v-model="commonSettings"
 | |
|                 :items="commonSettingsForm"
 | |
|               />
 | |
|             </v-container>
 | |
|             <v-stepper-actions
 | |
|               :disabled="isSubmitting"
 | |
|               prev-text="general.back"
 | |
|               next-text="general.next"
 | |
|               @click:prev="onPrev"
 | |
|               @click:next="onNext"
 | |
|             />
 | |
|           </v-stepper-window-item>
 | |
| 
 | |
|           <!-- CONFIRMATION -->
 | |
|           <v-stepper-window-item :value="Pages.CONFIRM">
 | |
|             <v-container max-width="880">
 | |
|               <v-card-title class="headline pa-0">
 | |
|                 {{ $t('general.confirm-how-does-everything-look') }}
 | |
|               </v-card-title>
 | |
|               <v-list>
 | |
|                 <template v-for="(item, idx) in confirmationData">
 | |
|                   <v-list-item
 | |
|                     v-if="item.display"
 | |
|                     :key="idx"
 | |
|                     class="px-0"
 | |
|                   >
 | |
|                     <v-list-item-title>{{ item.text }}</v-list-item-title>
 | |
|                     <v-list-item-subtitle>{{ item.value }}</v-list-item-subtitle>
 | |
|                   </v-list-item>
 | |
|                   <v-divider
 | |
|                     v-if="idx !== confirmationData.length - 1"
 | |
|                     :key="`divider-${idx}`"
 | |
|                   />
 | |
|                 </template>
 | |
|               </v-list>
 | |
|             </v-container>
 | |
|             <v-stepper-actions
 | |
|               :disabled="isSubmitting"
 | |
|               prev-text="general.back"
 | |
|               @click:prev="onPrev"
 | |
|             >
 | |
|               <template #next>
 | |
|                 <BaseButton
 | |
|                   create
 | |
|                   flat
 | |
|                   :disabled="isSubmitting"
 | |
|                   :loading="isSubmitting"
 | |
|                   :icon="$globals.icons.check"
 | |
|                   :text="$t('general.submit')"
 | |
|                   @click="onNext"
 | |
|                 />
 | |
|               </template>
 | |
|             </v-stepper-actions>
 | |
|           </v-stepper-window-item>
 | |
| 
 | |
|           <!-- END -->
 | |
|           <v-stepper-window-item :value="Pages.END">
 | |
|             <v-container max-width="880">
 | |
|               <v-card-title class="text-h4 justify-center">
 | |
|                 {{ $t('admin.setup.setup-complete') }}
 | |
|               </v-card-title>
 | |
|               <v-card-title class="text-h6 justify-center">
 | |
|                 {{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
 | |
|               </v-card-title>
 | |
|               <div
 | |
|                 v-for="link, idx in setupCompleteLinks"
 | |
|                 :key="idx"
 | |
|                 class="px-4 pt-4"
 | |
|               >
 | |
|                 <div v-if="link.section">
 | |
|                   <v-divider v-if="idx" />
 | |
|                   <v-card-text class="headline pl-0">
 | |
|                     {{ link.section }}
 | |
|                   </v-card-text>
 | |
|                 </div>
 | |
|                 <v-btn
 | |
|                   :to="link.to"
 | |
|                   color="info"
 | |
|                 >
 | |
|                   {{ link.text }}
 | |
|                 </v-btn>
 | |
|                 <v-card-text class="subtitle px-0 py-2">
 | |
|                   {{ link.description }}
 | |
|                 </v-card-text>
 | |
|               </div>
 | |
|             </v-container>
 | |
|             <v-stepper-actions
 | |
|               :disabled="isSubmitting"
 | |
|               prev-text="general.back"
 | |
|               @click:prev="onPrev"
 | |
|             >
 | |
|               <template #next>
 | |
|                 <BaseButton
 | |
|                   flat
 | |
|                   color="primary"
 | |
|                   :disabled="isSubmitting"
 | |
|                   :loading="isSubmitting"
 | |
|                   :icon="$globals.icons.home"
 | |
|                   :text="$t('general.home')"
 | |
|                   @click="onFinish"
 | |
|                 />
 | |
|               </template>
 | |
|             </v-stepper-actions>
 | |
|           </v-stepper-window-item>
 | |
|         </v-stepper-window>
 | |
|       </v-stepper>
 | |
| 
 | |
|       <!-- Dialog Language -->
 | |
|       <LanguageDialog v-model="langDialog" />
 | |
|     </v-card>
 | |
|   </v-container>
 | |
| </template>
 | |
| 
 | |
| <script setup lang="ts">
 | |
| import { useDark } from "@vueuse/core";
 | |
| import { useAdminApi, useUserApi } from "~/composables/api";
 | |
| import { useLocales } from "~/composables/use-locales";
 | |
| import { alert } from "~/composables/use-toast";
 | |
| import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
 | |
| import { useCommonSettingsForm } from "~/composables/use-setup/common-settings-form";
 | |
| import UserRegistrationForm from "~/components/Domain/User/UserRegistrationForm.vue";
 | |
| 
 | |
| definePageMeta({
 | |
|   layout: "blank",
 | |
|   middleware: ["admin-only"],
 | |
| });
 | |
| 
 | |
| // ================================================================
 | |
| // Setup
 | |
| const i18n = useI18n();
 | |
| const $auth = useMealieAuth();
 | |
| const userApi = useUserApi();
 | |
| const adminApi = useAdminApi();
 | |
| 
 | |
| const groupSlug = computed(() => $auth.user.value?.groupSlug);
 | |
| const { locale } = useLocales();
 | |
| const router = useRouter();
 | |
| const isSubmitting = ref(false);
 | |
| const langDialog = ref(false);
 | |
| const isDark = useDark();
 | |
| 
 | |
| useSeoMeta({
 | |
|   title: i18n.t("admin.setup.first-time-setup"),
 | |
| });
 | |
| 
 | |
| enum Pages {
 | |
|   LANDING = 0,
 | |
|   USER_INFO = 1,
 | |
|   PAGE_2 = 2,
 | |
|   CONFIRM = 3,
 | |
|   END = 4,
 | |
| }
 | |
| 
 | |
| // ================================================================
 | |
| // Forms
 | |
| const { accountDetails, credentials } = useUserRegistrationForm();
 | |
| const { commonSettingsForm } = useCommonSettingsForm();
 | |
| const commonSettings = ref({
 | |
|   makeGroupRecipesPublic: false,
 | |
|   useSeedData: true,
 | |
| });
 | |
| 
 | |
| const confirmationData = computed(() => {
 | |
|   return [
 | |
|     {
 | |
|       display: true,
 | |
|       text: i18n.t("user.email"),
 | |
|       value: accountDetails.email.value,
 | |
|     },
 | |
|     {
 | |
|       display: true,
 | |
|       text: i18n.t("user.username"),
 | |
|       value: accountDetails.username.value,
 | |
|     },
 | |
|     {
 | |
|       display: true,
 | |
|       text: i18n.t("user.full-name"),
 | |
|       value: accountDetails.fullName.value,
 | |
|     },
 | |
|     {
 | |
|       display: true,
 | |
|       text: i18n.t("user.enable-advanced-content"),
 | |
|       value: accountDetails.advancedOptions.value ? i18n.t("general.yes") : i18n.t("general.no"),
 | |
|     },
 | |
|     {
 | |
|       display: true,
 | |
|       text: i18n.t("group.enable-public-access"),
 | |
|       value: commonSettings.value.makeGroupRecipesPublic ? i18n.t("general.yes") : i18n.t("general.no"),
 | |
|     },
 | |
|     {
 | |
|       display: true,
 | |
|       text: i18n.t("user-registration.use-seed-data"),
 | |
|       value: commonSettings.value.useSeedData ? i18n.t("general.yes") : i18n.t("general.no"),
 | |
|     },
 | |
|   ];
 | |
| });
 | |
| 
 | |
| const setupCompleteLinks = ref([
 | |
|   {
 | |
|     section: i18n.t("profile.data-migrations"),
 | |
|     to: "/admin/backups",
 | |
|     text: i18n.t("settings.backup.backup-restore"),
 | |
|     description: i18n.t("admin.setup.restore-from-v1-backup"),
 | |
|   },
 | |
|   {
 | |
|     to: "/group/migrations",
 | |
|     text: i18n.t("migration.recipe-migration"),
 | |
|     description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
 | |
|   },
 | |
|   {
 | |
|     section: i18n.t("recipe.create-recipes"),
 | |
|     to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
 | |
|     text: i18n.t("recipe.create-recipe"),
 | |
|     description: i18n.t("recipe.create-recipe-description"),
 | |
|   },
 | |
|   {
 | |
|     to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
 | |
|     text: i18n.t("recipe.import-with-url"),
 | |
|     description: i18n.t("recipe.scrape-recipe-description"),
 | |
|   },
 | |
|   {
 | |
|     section: i18n.t("user.manage-users"),
 | |
|     to: "/admin/manage/users",
 | |
|     text: i18n.t("user.manage-users"),
 | |
|     description: i18n.t("user.manage-users-description"),
 | |
|   },
 | |
|   {
 | |
|     to: "/user/profile",
 | |
|     text: i18n.t("profile.manage-user-profile"),
 | |
|     description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
 | |
|   },
 | |
| ]);
 | |
| 
 | |
| // ================================================================
 | |
| // Page Navigation
 | |
| const currentPage = ref(Pages.LANDING);
 | |
| 
 | |
| // ================================================================
 | |
| // Page Submission
 | |
| 
 | |
| async function updateUser() {
 | |
|   // Note: $auth.user is now a ref
 | |
|   const { response } = await userApi.users.updateOne($auth.user.value!.id, {
 | |
|     ...$auth.user.value,
 | |
|     email: accountDetails.email.value,
 | |
|     username: accountDetails.username.value,
 | |
|     fullName: accountDetails.fullName.value,
 | |
|     advanced: accountDetails.advancedOptions.value,
 | |
|   });
 | |
| 
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
|   else {
 | |
|     $auth.refresh();
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function updatePassword() {
 | |
|   const { response } = await userApi.users.changePassword({
 | |
|     currentPassword: "MyPassword",
 | |
|     newPassword: credentials.password1.value,
 | |
|   });
 | |
| 
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function submitRegistration() {
 | |
|   // we update the password first, then update the user's details
 | |
|   await updatePassword().then(updateUser);
 | |
| }
 | |
| 
 | |
| async function updateGroup() {
 | |
|   // Note: $auth.user is now a ref
 | |
|   const { data } = await userApi.groups.getOne($auth.user.value!.groupId);
 | |
|   if (!data || !data.preferences) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const preferences = {
 | |
|     ...data.preferences,
 | |
|     privateGroup: !commonSettings.value.makeGroupRecipesPublic,
 | |
|   };
 | |
| 
 | |
|   const payload = {
 | |
|     ...data,
 | |
|     preferences,
 | |
|   };
 | |
| 
 | |
|   // Note: $auth.user is now a ref
 | |
|   const { response } = await userApi.groups.updateOne($auth.user.value!.groupId, payload);
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function updateHousehold() {
 | |
|   // Note: $auth.user is now a ref
 | |
|   const { data } = await adminApi.households.getOne($auth.user.value!.householdId);
 | |
|   if (!data || !data.preferences) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const preferences = {
 | |
|     ...data.preferences,
 | |
|     privateHousehold: !commonSettings.value.makeGroupRecipesPublic,
 | |
|     recipePublic: commonSettings.value.makeGroupRecipesPublic,
 | |
|   };
 | |
| 
 | |
|   const payload = {
 | |
|     ...data,
 | |
|     preferences,
 | |
|   };
 | |
| 
 | |
|   // Note: $auth.user is now a ref
 | |
|   const { response } = await adminApi.households.updateOne($auth.user.value!.householdId, payload);
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function seedFoods() {
 | |
|   const { response } = await userApi.seeders.foods({ locale: locale.value });
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function seedUnits() {
 | |
|   const { response } = await userApi.seeders.units({ locale: locale.value });
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function seedLabels() {
 | |
|   const { response } = await userApi.seeders.labels({ locale: locale.value });
 | |
|   if (!response || response.status !== 200) {
 | |
|     alert.error(i18n.t("events.something-went-wrong"));
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function seedData() {
 | |
|   if (!commonSettings.value.useSeedData) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const tasks = [
 | |
|     seedFoods(),
 | |
|     seedUnits(),
 | |
|     seedLabels(),
 | |
|   ];
 | |
| 
 | |
|   await Promise.all(tasks);
 | |
| }
 | |
| 
 | |
| async function submitCommonSettings() {
 | |
|   const tasks = [
 | |
|     updateGroup(),
 | |
|     updateHousehold(),
 | |
|     seedData(),
 | |
|   ];
 | |
| 
 | |
|   await Promise.all(tasks);
 | |
| }
 | |
| 
 | |
| async function submitAll() {
 | |
|   const tasks = [
 | |
|     submitRegistration(),
 | |
|     submitCommonSettings(),
 | |
|   ];
 | |
| 
 | |
|   await Promise.all(tasks);
 | |
| }
 | |
| 
 | |
| async function handleSubmit(page: number) {
 | |
|   if (isSubmitting.value) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   isSubmitting.value = true;
 | |
|   switch (page) {
 | |
|     case Pages.USER_INFO:
 | |
|       if (await accountDetails.validate()) {
 | |
|         currentPage.value += 1;
 | |
|       }
 | |
|       break;
 | |
|     case Pages.CONFIRM:
 | |
|       await submitAll();
 | |
|       currentPage.value += 1;
 | |
|       break;
 | |
|     case Pages.END:
 | |
|       router.push(groupSlug.value ? `/g/${groupSlug.value}` : "/login");
 | |
|       break;
 | |
|   }
 | |
|   isSubmitting.value = false;
 | |
| }
 | |
| 
 | |
| // ================================================================
 | |
| // Stepper Navigation Handlers
 | |
| function onPrev() {
 | |
|   if (isSubmitting.value) return;
 | |
|   if (currentPage.value > Pages.LANDING) currentPage.value -= 1;
 | |
| }
 | |
| 
 | |
| async function onNext() {
 | |
|   if (isSubmitting.value) return;
 | |
|   if (currentPage.value === Pages.USER_INFO) {
 | |
|     await handleSubmit(Pages.USER_INFO);
 | |
|     return;
 | |
|   }
 | |
|   if (currentPage.value === Pages.CONFIRM) {
 | |
|     await handleSubmit(Pages.CONFIRM);
 | |
|     return;
 | |
|   }
 | |
|   currentPage.value += 1;
 | |
| }
 | |
| 
 | |
| async function onFinish() {
 | |
|   if (isSubmitting.value) return;
 | |
|   await handleSubmit(Pages.END);
 | |
| }
 | |
| </script>
 | |
| 
 | |
| <style scoped>
 | |
| .icon-white {
 | |
|   fill: white;
 | |
| }
 | |
| 
 | |
| .icon-container {
 | |
|   display: flex;
 | |
|   flex-direction: column;
 | |
|   align-items: center;
 | |
|   width: 100%;
 | |
|   position: relative;
 | |
|   margin-top: 2.5rem;
 | |
| }
 | |
| 
 | |
| .icon-divider {
 | |
|   width: 100%;
 | |
|   margin-bottom: -2.5rem;
 | |
| }
 | |
| 
 | |
| .icon-avatar {
 | |
|   border-color: rgba(0, 0, 0, 0.12);
 | |
|   border: 2px;
 | |
| }
 | |
| 
 | |
| .bg-off-white {
 | |
|   background: #f5f8fa;
 | |
| }
 | |
| </style>
 |