mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
Setup wizard & 0.3.0 release (#146)
* fix: small merge fixes * feat: initial setup wizard * fix: last few localization items * fix: lint * fix: bump version
This commit is contained in:
@ -1,80 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<Listbox v-model="wiredLocale" as="div">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-400">{{
|
||||
$t("selectLanguage")
|
||||
}}</ListboxLabel>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="grid w-full cursor-default grid-cols-1 rounded-md bg-zinc-900 py-1.5 pr-2 pl-3 text-left text-zinc-300 outline-1 -outline-offset-1 outline-zinc-700 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="col-start-1 row-start-1 flex items-center gap-3 pr-6">
|
||||
<EmojiText
|
||||
:emoji="localeToEmoji(wiredLocale)"
|
||||
class="-mt-0.5 shrink-0 max-w-6"
|
||||
/>
|
||||
<span class="block truncate">{{
|
||||
currentLocaleInformation?.name ?? wiredLocale
|
||||
}}</span>
|
||||
</span>
|
||||
<ChevronUpDownIcon
|
||||
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="listLocale in locales"
|
||||
:key="listLocale.code"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="listLocale.code"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-hidden'
|
||||
: 'text-zinc-300',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<EmojiText
|
||||
:emoji="localeToEmoji(listLocale.code)"
|
||||
class="-mt-0.5 shrink-0 max-w-6"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'ml-3 block truncate',
|
||||
]"
|
||||
>{{ listLocale.name }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
<LanguageSelectorListbox />
|
||||
<NuxtLink
|
||||
class="mt-1 transition text-blue-500 hover:text-blue-600 text-sm"
|
||||
to="https://translate.droposs.org/projects/drop/"
|
||||
@ -97,80 +23,3 @@
|
||||
</DevOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CheckIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import type { Locale } from "vue-i18n";
|
||||
|
||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
||||
|
||||
function changeLocale(locale: Locale) {
|
||||
setLocale(locale);
|
||||
|
||||
// dynamically update the HTML attributes for language and direction
|
||||
// this is necessary for proper rendering of the page in the new language
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: locale,
|
||||
dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function localeToEmoji(local: string): string {
|
||||
switch (local) {
|
||||
// Default locale
|
||||
case "en":
|
||||
case "en-us":
|
||||
return "🇺🇸";
|
||||
|
||||
case "en-gb":
|
||||
return "🇬🇧";
|
||||
case "en-ca":
|
||||
return "🇨🇦";
|
||||
case "en-au":
|
||||
return "🇦🇺";
|
||||
case "en-pirate":
|
||||
return "🏴☠️";
|
||||
case "fr":
|
||||
return "🇫🇷";
|
||||
case "de":
|
||||
return "🇩🇪";
|
||||
case "es":
|
||||
return "🇪🇸";
|
||||
case "it":
|
||||
return "🇮🇹";
|
||||
case "zh":
|
||||
return "🇨🇳";
|
||||
case "zh-tw":
|
||||
return "🇹🇼";
|
||||
|
||||
default: {
|
||||
return "❓";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wiredLocale = computed({
|
||||
get() {
|
||||
return currLocale.value;
|
||||
},
|
||||
set(v) {
|
||||
changeLocale(v);
|
||||
},
|
||||
});
|
||||
const currentLocaleInformation = computed(() =>
|
||||
locales.value.find((e) => e.code == wiredLocale.value),
|
||||
);
|
||||
</script>
|
||||
|
||||
155
components/LanguageSelectorListbox.vue
Normal file
155
components/LanguageSelectorListbox.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<Listbox v-model="wiredLocale" as="div">
|
||||
<ListboxLabel
|
||||
v-if="showText"
|
||||
class="block text-sm/6 font-medium text-zinc-400"
|
||||
>{{ $t("selectLanguage") }}</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="grid w-full cursor-default grid-cols-1 rounded-md bg-zinc-900 py-1.5 pr-2 pl-3 text-left text-zinc-300 outline-1 -outline-offset-1 outline-zinc-700 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="col-start-1 row-start-1 flex items-center gap-3 pr-6">
|
||||
<EmojiText
|
||||
:emoji="localeToEmoji(wiredLocale)"
|
||||
class="-mt-0.5 shrink-0 max-w-6"
|
||||
/>
|
||||
<span class="block truncate">{{
|
||||
currentLocaleInformation?.name ?? wiredLocale
|
||||
}}</span>
|
||||
</span>
|
||||
<ChevronUpDownIcon
|
||||
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="listLocale in locales"
|
||||
:key="listLocale.code"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="listLocale.code"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-600 text-white outline-hidden'
|
||||
: 'text-zinc-300',
|
||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<EmojiText
|
||||
:emoji="localeToEmoji(listLocale.code)"
|
||||
class="-mt-0.5 shrink-0 max-w-6"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'ml-3 block truncate',
|
||||
]"
|
||||
>{{ listLocale.name }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||
import { CheckIcon } from "@heroicons/vue/24/outline";
|
||||
import type { Locale } from "vue-i18n";
|
||||
|
||||
const { showText = true } = defineProps<{ showText?: boolean }>();
|
||||
|
||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
||||
|
||||
function changeLocale(locale: Locale) {
|
||||
setLocale(locale);
|
||||
|
||||
// dynamically update the HTML attributes for language and direction
|
||||
// this is necessary for proper rendering of the page in the new language
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: locale,
|
||||
dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function localeToEmoji(local: string): string {
|
||||
switch (local) {
|
||||
// Default locale
|
||||
case "en":
|
||||
case "en-us":
|
||||
return "🇺🇸";
|
||||
|
||||
case "en-gb":
|
||||
return "🇬🇧";
|
||||
case "en-ca":
|
||||
return "🇨🇦";
|
||||
case "en-au":
|
||||
return "🇦🇺";
|
||||
case "en-pirate":
|
||||
return "🏴☠️";
|
||||
case "fr":
|
||||
return "🇫🇷";
|
||||
case "de":
|
||||
return "🇩🇪";
|
||||
case "es":
|
||||
return "🇪🇸";
|
||||
case "it":
|
||||
return "🇮🇹";
|
||||
case "zh":
|
||||
return "🇨🇳";
|
||||
case "zh-tw":
|
||||
return "🇹🇼";
|
||||
|
||||
default: {
|
||||
return "❓";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wiredLocale = computed({
|
||||
get() {
|
||||
return currLocale.value;
|
||||
},
|
||||
set(v) {
|
||||
changeLocale(v);
|
||||
},
|
||||
});
|
||||
const currentLocaleInformation = computed(() =>
|
||||
locales.value.find((e) => e.code == wiredLocale.value),
|
||||
);
|
||||
</script>
|
||||
159
components/Setup/Account.vue
Normal file
159
components/Setup/Account.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="p-2 lg:p-4">
|
||||
<div class="px-4 py-2 max-w-xl">
|
||||
<h1 class="font-semibold text-zinc-100 text-xl">
|
||||
{{ $t("setup.auth.title") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
{{ $t("setup.auth.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 xl:grid-cols-3 h-fit p-4 gap-4">
|
||||
<div class="p-4 border-1 border-zinc-800 rounded-xl">
|
||||
<div>
|
||||
<h1 class="text-zinc-100 font-semibold text-lg">
|
||||
{{ $t("setup.auth.simple.title") }}
|
||||
</h1>
|
||||
<p class="text-sm text-zinc-400">
|
||||
{{ $t("setup.auth.simple.description") }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||
href="https://docs.droposs.org/docs/authentication/simple"
|
||||
target="_blank"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="setup.auth.docs"
|
||||
tag="span"
|
||||
class="inline-flex items-center gap-x-1"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<span class="text-zinc-100 font-semibold text-sm">{{
|
||||
$t("setup.auth.enabled")
|
||||
}}</span>
|
||||
<CheckIcon
|
||||
v-if="enabledAuth.Simple"
|
||||
class="size-5 text-green-600"
|
||||
/>
|
||||
<XMarkIcon v-else class="size-5 text-red-600" />
|
||||
</div>
|
||||
<LoadingButton
|
||||
class="mt-4"
|
||||
:loading="invitationLoading"
|
||||
:disabled="!enabledAuth.Simple"
|
||||
@click="() => registerAsAdmin()"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="setup.auth.simple.register"
|
||||
tag="span"
|
||||
class="inline-flex items-center gap-x-1"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
{{ $t("chars.arrow") }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border-1 border-zinc-800 rounded-xl">
|
||||
<div>
|
||||
<h1 class="text-zinc-100 font-semibold text-lg">
|
||||
{{ $t("setup.auth.openid.title") }}
|
||||
</h1>
|
||||
<p class="text-sm text-zinc-400">
|
||||
{{ $t("setup.auth.openid.description") }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||
href="https://docs.droposs.org/docs/authentication/oidc"
|
||||
target="_blank"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="setup.auth.docs"
|
||||
tag="span"
|
||||
class="inline-flex items-center gap-x-1"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<span class="text-zinc-100 font-semibold text-sm">{{
|
||||
$t("setup.auth.enabled")
|
||||
}}</span>
|
||||
<CheckIcon
|
||||
v-if="enabledAuth.OpenID"
|
||||
class="size-5 text-green-600"
|
||||
/>
|
||||
<XMarkIcon v-else class="size-5 text-red-600" />
|
||||
</div>
|
||||
<LoadingButton
|
||||
class="mt-4"
|
||||
:loading="false"
|
||||
:disabled="!enabledAuth.OpenID"
|
||||
@click="() => (complete = true)"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="setup.auth.openid.skip"
|
||||
tag="span"
|
||||
class="inline-flex items-center gap-x-1"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
{{ $t("chars.arrow") }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const complete = defineModel<boolean>({ required: true });
|
||||
|
||||
const { token } = defineProps<{ token: string }>();
|
||||
|
||||
const invitationLoading = ref(false);
|
||||
|
||||
const enabledAuth = await $dropFetch("/api/v1/admin/auth", {
|
||||
headers: { Authorization: token },
|
||||
});
|
||||
|
||||
async function registerAsAdmin() {
|
||||
invitationLoading.value = true;
|
||||
const expiryDate = DateTime.now().plus({ year: 5000 }).toJSON();
|
||||
|
||||
const invitation = await $dropFetch("/api/v1/admin/auth/invitation", {
|
||||
method: "POST",
|
||||
body: { isAdmin: true, expires: expiryDate },
|
||||
headers: { Authorization: token },
|
||||
failTitle: "Failed to create admin invitation",
|
||||
});
|
||||
|
||||
window.open(`${invitation.inviteUrl}&after=close`, "_blank")?.focus();
|
||||
invitationLoading.value = false;
|
||||
complete.value = true;
|
||||
}
|
||||
</script>
|
||||
15
components/Setup/Library.vue
Normal file
15
components/Setup/Library.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="p-8">
|
||||
<AdminSourcesPage :token="token" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminSourcesPage from "~/pages/admin/library/sources/index.vue";
|
||||
|
||||
const complete = defineModel<boolean>({ required: true });
|
||||
// Only runs on component load, so it's fine
|
||||
complete.value = true;
|
||||
|
||||
const { token } = defineProps<{ token: string }>();
|
||||
</script>
|
||||
Reference in New Issue
Block a user