Browse Source

footer and header in setup, remove lang setup step

pull/1665/head
Bernd Storath 6 months ago
parent
commit
323b61a7ef
  1. 44
      src/app/components/header/LangSelector.vue
  2. 2
      src/app/components/header/Update.vue
  3. 15
      src/app/components/icons/Language.vue
  4. 38
      src/app/components/ui/Footer.vue
  5. 42
      src/app/layouts/default.vue
  6. 13
      src/app/layouts/setup.vue
  7. 10
      src/app/pages/setup/1.vue
  8. 75
      src/app/pages/setup/2.vue
  9. 72
      src/app/pages/setup/3.vue
  10. 62
      src/app/pages/setup/4.vue
  11. 71
      src/app/pages/setup/5.vue
  12. 14
      src/app/stores/setup.ts
  13. 2
      src/server/api/setup/2.post.ts
  14. 0
      src/server/api/setup/4.post.ts

44
src/app/components/header/LangSelector.vue

@ -0,0 +1,44 @@
<template>
<SelectRoot v-model="langProxy" :default-value="locale">
<SelectTrigger
class="inline-flex h-8 items-center justify-around gap-2 rounded bg-gray-200 px-3 text-sm leading-none dark:bg-neutral-700 dark:text-neutral-400"
aria-label="Select language"
>
<IconsLanguage class="size-3" />
<SelectValue />
<IconsArrowDown class="size-3" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="min-w-28 rounded bg-gray-300 dark:bg-neutral-500"
position="popper"
>
<SelectViewport class="p-2">
<SelectItem
v-for="(option, index) in langs"
:key="index"
:value="option.code"
class="relative flex h-6 items-center rounded px-3 text-sm leading-none outline-none hover:bg-red-800 hover:text-white data-[state=checked]:underline dark:text-white"
>
<SelectItemText>
{{ option.name }}
</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
<script setup lang="ts">
const { locales, locale, setLocale } = useI18n();
const langProxy = ref(locale);
watchEffect(() => {
setLocale(langProxy.value);
});
const langs = locales.value.sort((a, b) => a.code.localeCompare(b.code));
</script>

2
src/app/components/header/Update.vue

@ -23,4 +23,6 @@
<script lang="ts" setup> <script lang="ts" setup>
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
// TODO: only show this to admins
</script> </script>

15
src/app/components/icons/Language.vue

@ -0,0 +1,15 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"
/>
</svg>
</template>

38
src/app/components/ui/Footer.vue

@ -0,0 +1,38 @@
<template>
<footer>
<p class="m-10 text-center text-xs text-gray-300 dark:text-neutral-600">
<a
class="hover:underline"
target="_blank"
href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a
>
({{ globalStore.currentRelease }}) © 2021-2025 by
<a
class="hover:underline"
target="_blank"
href="https://emile.nl/?ref=wg-easy"
>Emile Nijssen</a
>
is licensed under
<a
class="hover:underline"
target="_blank"
href="https://spdx.org/licenses/AGPL-3.0-only.html"
>AGPL-3.0-only</a
>
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>{{ $t('donate') }}</a
>
</p>
</footer>
</template>
<script lang="ts" setup>
const globalStore = useGlobalStore();
globalStore.fetchRelease();
</script>

42
src/app/layouts/default.vue

@ -11,55 +11,21 @@
> >
<HeaderLogo v-if="loggedIn" /> <HeaderLogo v-if="loggedIn" />
<div class="flex grow-0 items-center gap-3 self-end xxs:self-center"> <div class="flex grow-0 items-center gap-3 self-end xxs:self-center">
<HeaderLangSelector />
<HeaderThemeSwitch /> <HeaderThemeSwitch />
<HeaderChartToggle /> <HeaderChartToggle v-if="loggedIn" />
<UiUserMenu v-if="loggedIn" /> <UiUserMenu v-if="loggedIn" />
</div> </div>
</div> </div>
<HeaderUpdate class="mt-5" /> <HeaderUpdate class="mt-5" />
</header> </header>
<slot /> <slot />
<footer> <UiFooter />
<p class="m-10 text-center text-xs text-gray-300 dark:text-neutral-600">
<a
class="hover:underline"
target="_blank"
href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a
>
({{ globalStore.currentRelease }}) © 2021-2024 by
<a
class="hover:underline"
target="_blank"
href="https://emile.nl/?ref=wg-easy"
>Emile Nijssen</a
>
is licensed under
<a
class="hover:underline"
target="_blank"
href="https://spdx.org/licenses/AGPL-3.0-only.html"
>AGPL-3.0-only</a
>
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>{{ $t('donate') }}</a
>
</p>
</footer>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const globalStore = useGlobalStore();
globalStore.fetchRelease();
const route = useRoute(); const route = useRoute();
const loggedIn = computed( const loggedIn = computed(() => route.path !== '/login');
() => route.path !== '/login' && route.path !== '/setup'
);
</script> </script>

13
src/app/layouts/setup.vue

@ -1,6 +1,15 @@
<template> <template>
<main class="container mx-auto px-4"> <div>
<header class="container mx-auto mt-4 max-w-3xl px-3 xs:mt-6 md:px-0">
<div class="mb-5 flex justify-end">
<div class="flex grow-0 items-center gap-3 self-end xxs:self-center">
<HeaderLangSelector />
<HeaderThemeSwitch />
</div>
</div>
<UiBanner /> <UiBanner />
</header>
<main>
<Panel> <Panel>
<PanelBody class="mx-auto mt-10 p-4 md:w-[70%] lg:w-[60%]"> <PanelBody class="mx-auto mt-10 p-4 md:w-[70%] lg:w-[60%]">
<h2 class="mb-16 mt-8 text-3xl font-medium"> <h2 class="mb-16 mt-8 text-3xl font-medium">
@ -18,6 +27,8 @@
</PanelBody> </PanelBody>
</Panel> </Panel>
</main> </main>
<UiFooter />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

10
src/app/pages/setup/1.vue

@ -1,22 +1,16 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="px-8 pt-8 text-center text-2xl">
{{ $t('setup.messageSetupLanguage') }} {{ $t('setup.messageWelcome.whatIs') }}
</p> </p>
<div class="mb-8 flex justify-center">
<UiChooseLang />
</div>
<div>
<NuxtLink to="/setup/2"><BaseButton>Continue</BaseButton></NuxtLink> <NuxtLink to="/setup/2"><BaseButton>Continue</BaseButton></NuxtLink>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'setup', layout: 'setup',
}); });
const setupStore = useSetupStore(); const setupStore = useSetupStore();
setupStore.setStep(1); setupStore.setStep(1);
</script> </script>

75
src/app/pages/setup/2.vue

@ -1,16 +1,83 @@
<template> <template>
<div> <div>
<p class="px-8 pt-8 text-center text-2xl"> <p class="p-8 text-center text-lg">
{{ $t('setup.messageWelcome.whatIs') }} {{ $t('setup.messageSetupCreateAdminUser') }}
</p> </p>
<NuxtLink to="/setup/3"><BaseButton>Continue</BaseButton></NuxtLink> <form id="newAccount"></form>
<div>
<Label for="username">{{ $t('username') }}</Label>
<input
id="username"
v-model="username"
form="newAccount"
type="text"
autocomplete="username"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="password">{{ $t('setup.newPassword') }}</Label>
<input
id="password"
v-model="password"
form="newAccount"
type="password"
autocomplete="new-password"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="accept">{{ $t('setup.accept') }}</Label>
<input
id="accept"
v-model="accept"
form="newAccount"
type="checkbox"
class="ml-2"
/>
</div>
<BaseButton @click="newAccount">Create Account</BaseButton>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { FetchError } from 'ofetch';
const { t } = useI18n();
definePageMeta({ definePageMeta({
layout: 'setup', layout: 'setup',
}); });
const setupStore = useSetupStore(); const setupStore = useSetupStore();
setupStore.setStep(2); setupStore.setStep(2);
const router = useRouter();
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
const toast = useToast();
async function newAccount() {
try {
if (!username.value || !password.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
await setupStore.step2(username.value, password.value, accept.value);
await router.push('/setup/3');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script> </script>

72
src/app/pages/setup/3.vue

@ -1,83 +1,19 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupCreateAdminUser') }} {{ 'Do you have a existing Setup?' }}
</p> </p>
<form id="newAccount"></form> <div class="mb-8 flex justify-center">
<div> <NuxtLink to="/setup/4"><BaseButton>No</BaseButton></NuxtLink>
<Label for="username">{{ $t('username') }}</Label> <NuxtLink to="/setup/migrate"><BaseButton>Yes</BaseButton></NuxtLink>
<input
id="username"
v-model="username"
form="newAccount"
type="text"
autocomplete="username"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="password">{{ $t('setup.newPassword') }}</Label>
<input
id="password"
v-model="password"
form="newAccount"
type="password"
autocomplete="new-password"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div> </div>
<div>
<Label for="accept">{{ $t('setup.accept') }}</Label>
<input
id="accept"
v-model="accept"
form="newAccount"
type="checkbox"
class="ml-2"
/>
</div>
<BaseButton @click="newAccount">Create Account</BaseButton>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { FetchError } from 'ofetch';
const { t } = useI18n();
definePageMeta({ definePageMeta({
layout: 'setup', layout: 'setup',
}); });
const setupStore = useSetupStore(); const setupStore = useSetupStore();
setupStore.setStep(3); setupStore.setStep(3);
const router = useRouter();
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
const toast = useToast();
async function newAccount() {
try {
if (!username.value || !password.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
await setupStore.step3(username.value, password.value, accept.value);
await router.push('/setup/4');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script> </script>

62
src/app/pages/setup/4.vue

@ -1,19 +1,71 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="p-8 text-center text-lg">
{{ 'Do you have a existing Setup?' }} {{ $t('setup.messageSetupHostPort') }}
</p> </p>
<div class="mb-8 flex justify-center"> <div>
<NuxtLink to="/setup/5"><BaseButton>No</BaseButton></NuxtLink> <Label for="host">{{ $t('setup.host') }}</Label>
<NuxtLink to="/setup/migrate"><BaseButton>Yes</BaseButton></NuxtLink> <input
id="host"
v-model="host"
type="text"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
placeholder="vpn.example.com"
/>
</div>
<div>
<Label for="port">{{ $t('setup.port') }}</Label>
<input
id="port"
v-model="port"
type="number"
:min="1"
:max="65535"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div> </div>
<BaseButton @click="updateHostPort">Continue</BaseButton>
</div> </div>
</template> </template>
<script lang="ts" setup> <script setup lang="ts">
import { FetchError } from 'ofetch';
definePageMeta({ definePageMeta({
layout: 'setup', layout: 'setup',
}); });
const { t } = useI18n();
const setupStore = useSetupStore(); const setupStore = useSetupStore();
setupStore.setStep(4); setupStore.setStep(4);
const router = useRouter();
const host = ref<null | string>(null);
const port = ref<number>(51820);
const toast = useToast();
async function updateHostPort() {
if (!host.value || !port.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
try {
await setupStore.step4(host.value, port.value);
await router.push('/setup/success');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script> </script>

71
src/app/pages/setup/5.vue

@ -1,71 +0,0 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupHostPort') }}
</p>
<div>
<Label for="host">{{ $t('setup.host') }}</Label>
<input
id="host"
v-model="host"
type="text"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
placeholder="vpn.example.com"
/>
</div>
<div>
<Label for="port">{{ $t('setup.port') }}</Label>
<input
id="port"
v-model="port"
type="number"
:min="1"
:max="65535"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<BaseButton @click="updateHostPort">Continue</BaseButton>
</div>
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
definePageMeta({
layout: 'setup',
});
const { t } = useI18n();
const setupStore = useSetupStore();
setupStore.setStep(5);
const router = useRouter();
const host = ref<null | string>(null);
const port = ref<number>(51820);
const toast = useToast();
async function updateHostPort() {
if (!host.value || !port.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
try {
await setupStore.step5(host.value, port.value);
await router.push('/setup/success');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script>

14
src/app/stores/setup.ts

@ -4,8 +4,8 @@ export const useSetupStore = defineStore('Setup', () => {
/** /**
* @throws if unsuccessful * @throws if unsuccessful
*/ */
async function step3(username: string, password: string, accept: boolean) { async function step2(username: string, password: string, accept: boolean) {
const response = await $fetch('/api/setup/3', { const response = await $fetch('/api/setup/2', {
method: 'post', method: 'post',
body: { username, password, accept }, body: { username, password, accept },
}); });
@ -15,8 +15,8 @@ export const useSetupStore = defineStore('Setup', () => {
/** /**
* @throws if unsuccessful * @throws if unsuccessful
*/ */
async function step5(host: string, port: number) { async function step4(host: string, port: number) {
const response = await $fetch('/api/setup/5', { const response = await $fetch('/api/setup/4', {
method: 'post', method: 'post',
body: { host, port }, body: { host, port },
}); });
@ -35,14 +35,14 @@ export const useSetupStore = defineStore('Setup', () => {
} }
const step = ref(1); const step = ref(1);
const totalSteps = ref(6); const totalSteps = ref(5);
function setStep(i: number) { function setStep(i: number) {
step.value = i; step.value = i;
} }
return { return {
step3, step2,
step5, step4,
runMigration, runMigration,
step, step,
totalSteps, totalSteps,

2
src/server/api/setup/3.post.ts → src/server/api/setup/2.post.ts

@ -9,6 +9,6 @@ export default defineSetupEventHandler(async ({ event }) => {
// TODO: validate setup step // TODO: validate setup step
await Database.users.create(username, password); await Database.users.create(username, password);
await Database.general.setSetupStep(4); await Database.general.setSetupStep(3);
return { success: true }; return { success: true };
}); });

0
src/server/api/setup/5.post.ts → src/server/api/setup/4.post.ts

Loading…
Cancel
Save