diff --git a/frontend/components/Domain/User/UserRegistrationForm.vue b/frontend/components/Domain/User/UserRegistrationForm.vue new file mode 100644 index 00000000000..8b1b945f9fd --- /dev/null +++ b/frontend/components/Domain/User/UserRegistrationForm.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/frontend/components/global/AutoForm.vue b/frontend/components/global/AutoForm.vue index f92acf2e975..2f143f52ea6 100644 --- a/frontend/components/global/AutoForm.vue +++ b/frontend/components/global/AutoForm.vue @@ -15,12 +15,22 @@ v-if="inputField.type === fieldTypes.BOOLEAN" v-model="value[inputField.varName]" class="my-0 py-0" - :label="inputField.label" :name="inputField.varName" - :hint="inputField.hint || ''" :disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))" @change="emitBlur" - /> + > + + + +
+ + +
+ + Mealie + + +
+ + + + + + +
+
+
+ +
+
+ +
+ + + + + {{ prevButtonIconRef }} + + {{ prevButtonTextRef }} + + + +
+ +
+
+ + {{ nextButtonIconRef }} + + {{ nextButtonTextRef }} + + {{ nextButtonIconRef }} + +
+
+
+ + + + {{ $t("language-dialog.choose-language") }} + + +
+
+ + + + + diff --git a/frontend/composables/use-setup/common-settings-form.ts b/frontend/composables/use-setup/common-settings-form.ts new file mode 100644 index 00000000000..12b3a7496e5 --- /dev/null +++ b/frontend/composables/use-setup/common-settings-form.ts @@ -0,0 +1,30 @@ +import { useContext } from "@nuxtjs/composition-api"; +import { fieldTypes } from "../forms"; +import { AutoFormItems } from "~/types/auto-forms"; + +export const useCommonSettingsForm = () => { + const { i18n } = useContext(); + + const commonSettingsForm: AutoFormItems = [ + { + section: i18n.tc("profile.group-settings"), + label: i18n.tc("group.enable-public-access"), + hint: i18n.tc("group.enable-public-access-description"), + varName: "makeGroupRecipesPublic", + type: fieldTypes.BOOLEAN, + rules: ["required"], + }, + { + section: i18n.tc("data-pages.data-management"), + label: i18n.tc("user-registration.use-seed-data"), + hint: i18n.tc("user-registration.use-seed-data-description"), + varName: "useSeedData", + type: fieldTypes.BOOLEAN, + rules: ["required"], + }, + ]; + + return { + commonSettingsForm, + } +} diff --git a/frontend/composables/use-setup/index.ts b/frontend/composables/use-setup/index.ts new file mode 100644 index 00000000000..607d0ede618 --- /dev/null +++ b/frontend/composables/use-setup/index.ts @@ -0,0 +1 @@ +export { useCommonSettingsForm } from "./common-settings-form"; diff --git a/frontend/composables/use-users/index.ts b/frontend/composables/use-users/index.ts index adacee24eb8..f1b1a6a8571 100644 --- a/frontend/composables/use-users/index.ts +++ b/frontend/composables/use-users/index.ts @@ -1 +1,2 @@ export { useUserForm } from "./user-form"; +export { useUserRegistrationForm } from "./user-registration-form"; diff --git a/frontend/composables/use-users/user-registration-form.ts b/frontend/composables/use-users/user-registration-form.ts new file mode 100644 index 00000000000..af27549bb1c --- /dev/null +++ b/frontend/composables/use-users/user-registration-form.ts @@ -0,0 +1,87 @@ +import { ref, Ref, useContext } from "@nuxtjs/composition-api"; +import { useAsyncValidator } from "~/composables/use-validators"; +import { VForm } from "~/types/vuetify"; +import { usePublicApi } from "~/composables/api/api-client"; + +const domAccountForm = ref(null); +const username = ref(""); +const fullName = ref(""); +const email = ref(""); +const password1 = ref(""); +const password2 = ref(""); +const advancedOptions = ref(false); + +export const useUserRegistrationForm = () => { + const { i18n } = useContext(); + function safeValidate(form: Ref) { + if (form.value && form.value.validate) { + return form.value.validate(); + } + return false; + } + // ================================================================ + // Provide Group Details + const publicApi = usePublicApi(); + // ================================================================ + // Provide Account Details + + const usernameErrorMessages = ref([]); + const { validate: validateUsername, valid: validUsername } = useAsyncValidator( + username, + (v: string) => publicApi.validators.username(v), + i18n.tc("validation.username-is-taken"), + usernameErrorMessages + ); + const emailErrorMessages = ref([]); + const { validate: validateEmail, valid: validEmail } = useAsyncValidator( + email, + (v: string) => publicApi.validators.email(v), + i18n.tc("validation.email-is-taken"), + emailErrorMessages + ); + const accountDetails = { + username, + fullName, + email, + advancedOptions, + validate: async () => { + if (!(validUsername.value && validEmail.value)) { + await Promise.all([validateUsername(), validateEmail()]); + } + + return (safeValidate(domAccountForm as Ref) && validUsername.value && validEmail.value); + }, + reset: () => { + accountDetails.username.value = ""; + accountDetails.fullName.value = ""; + accountDetails.email.value = ""; + accountDetails.advancedOptions.value = false; + }, + }; + // ================================================================ + // Provide Credentials + const passwordMatch = () => password1.value === password2.value || i18n.tc("user.password-must-match"); + const credentials = { + password1, + password2, + passwordMatch, + reset: () => { + credentials.password1.value = ""; + credentials.password2.value = ""; + } + }; + + return { + accountDetails, + credentials, + emailErrorMessages, + usernameErrorMessages, + // Fields + advancedOptions, + // Validators + validateUsername, + validateEmail, + // Dom Refs + domAccountForm, + }; +}; diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index aeda8952029..4cd655b7c31 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -85,6 +85,7 @@ "clear": "Clear", "close": "Close", "confirm": "Confirm", + "confirm-how-does-everything-look": "How does everything look?", "confirm-delete-generic": "Are you sure you want to delete this?", "copied_message": "Copied!", "create": "Create", @@ -170,6 +171,7 @@ "units": "Units", "back": "Back", "next": "Next", + "start": "Start", "toggle-view": "Toggle View", "date": "Date", "id": "Id", @@ -238,6 +240,8 @@ "group-preferences": "Group Preferences", "private-group": "Private Group", "private-group-description": "Setting your group to private will default all public view options to default. This overrides an individual recipes public view settings.", + "enable-public-access": "Enable Public Access", + "enable-public-access-description": "Make group recipes public by default, and allow visitors to view recipes without logging-in", "allow-users-outside-of-your-group-to-see-your-recipes": "Allow users outside of your group to see your recipes", "allow-users-outside-of-your-group-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your group or with a pre-generated private link", "show-nutrition-information": "Show nutrition information", @@ -351,6 +355,7 @@ }, "recipe-data-migrations": "Recipe Data Migrations", "recipe-data-migrations-explanation": "Recipes can be migrated from another supported application to Mealie. This is a great way to get started with Mealie.", + "coming-from-another-application-or-an-even-older-version-of-mealie": "Coming from another application or an even older version of Mealie? Check out migrations and see if your data can be imported.", "choose-migration-type": "Choose Migration Type", "tag-all-recipes": "Tag all recipes with {tag-name} tag", "nextcloud-text": "Nextcloud recipes can be imported from a zip file that contains the data stored in Nextcloud. See the example folder structure below to ensure your recipes are able to be imported.", @@ -533,6 +538,8 @@ "looking-for-migrations": "Looking For Migrations?", "import-with-url": "Import with URL", "create-recipe": "Create Recipe", + "create-recipe-description": "Create a new recipe from scratch.", + "create-recipes": "Create Recipes", "import-with-zip": "Import with .zip", "create-recipe-from-an-image": "Create recipe from an image", "bulk-url-import": "Bulk URL Import", @@ -843,6 +850,7 @@ "or": "or", "logout": "Logout", "manage-users": "Manage Users", + "manage-users-description": "Create and manage users.", "new-password": "New Password", "new-user": "New User", "password-has-been-reset-to-the-default-password": "Password has been reset to the default password", @@ -1138,7 +1146,17 @@ "background-tasks": "Background Tasks", "background-tasks-description": "Here you can view all the running background tasks and their status", "no-logs-found": "No Logs Found", - "tasks": "Tasks" + "tasks": "Tasks", + "setup": { + "first-time-setup": "First Time Setup", + "welcome-to-mealie-get-started": "Welcome to Mealie! Let's get started", + "already-set-up-bring-to-homepage": "I'm already set up, just bring me to the homepage", + "common-settings-for-new-sites": "Here are some common settings for new sites", + "setup-complete": "Setup Complete!", + "here-are-a-few-things-to-help-you-get-started": "Here are a few things to help you get started with Mealie", + "restore-from-v1-backup": "Have a backup from a previous instance of Mealie v1? You can restore it here.", + "manage-profile-or-get-invite-link": "Manage your own profile, or grab an invite link to share with others." + } }, "profile": { "welcome-user": "👋 Welcome, {0}", diff --git a/frontend/lib/api/types/admin.ts b/frontend/lib/api/types/admin.ts index 7f3e883c600..bdf98d9e9a0 100644 --- a/frontend/lib/api/types/admin.ts +++ b/frontend/lib/api/types/admin.ts @@ -43,6 +43,7 @@ export interface AppInfo { } export interface AppStartupInfo { isFirstLogin: boolean; + isDemo: boolean; } export interface AppStatistics { totalRecipes: number; diff --git a/frontend/lib/api/types/user.ts b/frontend/lib/api/types/user.ts index 32ec9767f33..970a05d2705 100644 --- a/frontend/lib/api/types/user.ts +++ b/frontend/lib/api/types/user.ts @@ -22,6 +22,7 @@ export interface CreateUserRegistration { groupToken?: string; email: string; username: string; + fullName: string; password: string; passwordConfirm: string; advanced?: boolean; diff --git a/frontend/lib/api/user/user-registration.ts b/frontend/lib/api/user/user-registration.ts index ea722c5fbc5..e1921d4cf3e 100644 --- a/frontend/lib/api/user/user-registration.ts +++ b/frontend/lib/api/user/user-registration.ts @@ -8,8 +8,6 @@ const routes = { }; export class RegisterAPI extends BaseAPI { - /** Returns a list of available .zip files for import into Mealie. - */ async register(payload: CreateUserRegistration) { return await this.requests.post(routes.register, payload); } diff --git a/frontend/pages/admin/setup.vue b/frontend/pages/admin/setup.vue new file mode 100644 index 00000000000..6541b289b56 --- /dev/null +++ b/frontend/pages/admin/setup.vue @@ -0,0 +1,431 @@ + + + diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index 795b423388c..c6fb9dc33ab 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -3,8 +3,9 @@ diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue index a595a1a911e..5815e3dc710 100644 --- a/frontend/pages/login.vue +++ b/frontend/pages/login.vue @@ -152,14 +152,8 @@ export default defineComponent({ const { $auth, i18n, $axios } = useContext(); const { loggedIn } = useLoggedInState(); const groupSlug = computed(() => $auth.user?.groupSlug); - - whenever( - () => loggedIn.value && groupSlug.value, - () => { - router.push(`/g/${groupSlug.value || ""}`); - }, - { immediate: true }, - ); + const isDemo = ref(false); + const isFirstLogin = ref(false); const form = reactive({ email: "", @@ -167,12 +161,23 @@ export default defineComponent({ remember: false, }); - const isFirstLogin = ref(false) - useAsync(async () => { const data = await $axios.get("/api/app/about/startup-info"); + isDemo.value = data.data.isDemo; isFirstLogin.value = data.data.isFirstLogin; - }, useAsyncKey()); + }, useAsyncKey()); + + whenever( + () => loggedIn.value && groupSlug.value, + () => { + if (!isDemo.value && isFirstLogin.value && $auth.user?.admin) { + router.push("/admin/setup"); + } else { + router.push(`/g/${groupSlug.value || ""}`); + } + }, + { immediate: true }, + ); const loggingIn = ref(false); diff --git a/frontend/pages/register/index.vue b/frontend/pages/register/index.vue index 1cd1f74d88a..fb42ed396ee 100644 --- a/frontend/pages/register/index.vue +++ b/frontend/pages/register/index.vue @@ -4,7 +4,7 @@ fluid class="d-flex justify-center align-center" :class="{ - 'bg-off-white': !$vuetify.theme.dark && !isDark.value, + 'bg-off-white': !$vuetify.theme.dark && !isDark, }" > @@ -136,73 +136,14 @@