mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-27 08:14:30 -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>
|