refactor: migrate to nuxt compatibilityVersion: 4 (#3298)

This commit is contained in:
Daniel Roe 2025-05-20 15:05:01 +01:00 committed by GitHub
parent 46e4433e1c
commit a3fbc056a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
342 changed files with 1200 additions and 2932 deletions

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
const mask = useMask()
</script>
<template>
<VDropdown :distance="0" placement="top-start" strategy="fixed" @apply-show="mask.show()" @apply-hide="mask.hide()">
<button btn-action-icon :aria-label="$t('action.switch_account')">
<div :class="{ 'hidden xl:block': currentUser }" i-ri:more-2-line />
<AccountAvatar v-if="currentUser" xl:hidden :account="currentUser.account" w-9 h-9 square />
</button>
<template #popper="{ hide }">
<UserSwitcher @click="hide" />
</template>
</VDropdown>
</template>

View file

@ -0,0 +1,46 @@
<script setup lang="ts">
import type { UserLogin } from '#shared/types'
const all = useUsers()
const router = useRouter()
function clickUser(user: UserLogin) {
if (user.account.acct === currentUser.value?.account.acct)
router.push(getAccountRoute(user.account))
else
switchUser(user)
}
</script>
<template>
<div flex justify-start items-end px-2 gap-5>
<div flex="~ wrap-reverse" gap-5>
<template v-for="user of all" :key="user.id">
<CommonTooltip :distance="8" :delay="{ show: 300, hide: 100 }">
<button
flex rounded
cursor-pointer
:aria-label="$t('action.switch_account')"
:class="user.account.acct === currentUser?.account.acct ? '' : 'op25 grayscale'"
hover="filter-none op100"
@click="clickUser(user)"
>
<AccountAvatar w-13 h-13 :account="user.account" square />
</button>
<template #popper>
<div text-center>
<span text-4>
<AccountDisplayName :account="user.account" />
</span>
<AccountHandle :account="user.account" />
</div>
</template>
</CommonTooltip>
</template>
</div>
<div flex items-center justify-center w-13 h-13>
<UserDropdown />
</div>
</div>
</template>

View file

@ -0,0 +1,182 @@
<script setup lang="ts">
import Fuse from 'fuse.js'
const input = ref<HTMLInputElement | undefined>()
const knownServers = ref<string[]>([])
const autocompleteIndex = ref(0)
const autocompleteShow = ref(false)
const { busy, error, displayError, server, oauth } = useSignIn(input)
const fuse = shallowRef(new Fuse([] as string[]))
const filteredServers = computed(() => {
if (!server.value)
return []
const results = fuse.value.search(server.value, { limit: 6 }).map(result => result.item)
if (results[0] === server.value)
return []
return results
})
function isValidUrl(str: string) {
try {
// eslint-disable-next-line no-new
new URL(str)
return true
}
catch {
return false
}
}
async function handleInput() {
const input = server.value.trim()
if (input.startsWith('https://'))
server.value = input.replace('https://', '')
if (input.length)
displayError.value = false
if (
isValidUrl(`https://${input}`)
&& input.match(/^[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?$/i)
// Do not hide the autocomplete if a result has an exact substring match on the input
&& !filteredServers.value.some(s => s.includes(input))
) {
autocompleteShow.value = false
}
else {
autocompleteShow.value = true
}
}
function toSelector(server: string) {
return server.replace(/[^\w-]/g, '-')
}
function move(delta: number) {
if (filteredServers.value.length === 0) {
autocompleteIndex.value = 0
return
}
autocompleteIndex.value = ((autocompleteIndex.value + delta) + filteredServers.value.length) % filteredServers.value.length
document.querySelector(`#${toSelector(filteredServers.value[autocompleteIndex.value])}`)?.scrollIntoView(false)
}
function onEnter(e: KeyboardEvent) {
if (autocompleteShow.value === true && filteredServers.value[autocompleteIndex.value]) {
server.value = filteredServers.value[autocompleteIndex.value]
e.preventDefault()
autocompleteShow.value = false
}
}
function escapeAutocomplete(evt: KeyboardEvent) {
if (!autocompleteShow.value)
return
autocompleteShow.value = false
evt.stopPropagation()
}
function select(index: number) {
server.value = filteredServers.value[index]
}
onMounted(async () => {
input?.value?.focus()
knownServers.value = await (globalThis.$fetch as any)('/api/list-servers')
fuse.value = new Fuse(knownServers.value, { shouldSort: true })
})
onClickOutside(input, () => {
autocompleteShow.value = false
})
</script>
<template>
<form text-center justify-center items-center max-w-150 py6 flex="~ col gap-3" @submit.prevent="oauth">
<div flex="~ center" items-end mb2 gap-x-2>
<img :src="`/${''}logo.svg`" w-12 h-12 mxa height="48" width="48" :alt="$t('app_logo')" class="rtl-flip">
<div text-3xl>
{{ $t('action.sign_in') }}
</div>
</div>
<div>
{{ $t('user.server_address_label') }}
</div>
<div :class="error ? 'animate animate-shake-x animate-delay-100' : null">
<div
dir="ltr"
flex bg-gray:10 px4 py2 mxa rounded
border="~ base" items-center font-mono
focus:outline-none focus:ring="2 primary inset"
relative
:class="displayError ? 'border-red-600 dark:border-red-400' : null"
>
<span text-secondary-light me1>https://</span>
<input
ref="input"
v-model="server"
autocapitalize="off"
inputmode="url"
outline-none bg-transparent w-full max-w-50
spellcheck="false"
autocorrect="off"
autocomplete="off"
@input="handleInput"
@keydown.down="move(1)"
@keydown.up="move(-1)"
@keydown.enter="onEnter"
@keydown.esc.prevent="escapeAutocomplete"
@focus="autocompleteShow = true"
>
<div
v-if="autocompleteShow && filteredServers.length"
absolute left-6em right-0 top="100%"
bg-base rounded border="~ base"
z-10 shadow of-auto
overflow-y-auto
class="max-h-[8rem]"
>
<button
v-for="(name, idx) in filteredServers"
:id="toSelector(name)"
:key="name"
:value="name"
px-2 py1 font-mono w-full text-left
:class="autocompleteIndex === idx ? 'text-primary font-bold' : null"
@click="select(idx)"
>
{{ name }}
</button>
</div>
</div>
<div min-h-4>
<Transition css enter-active-class="animate animate-fade-in">
<p v-if="displayError" role="alert" p-0 m-0 text-xs text-red-600 dark:text-red-400>
{{ $t('error.sign_in_error') }}
</p>
</Transition>
</div>
</div>
<div text-secondary text-sm flex>
<div i-ri:lightbulb-line me-1 />
<span>
<i18n-t keypath="user.tip_no_account">
<NuxtLink href="https://joinmastodon.org/servers" target="_blank" external class="text-primary" hover="underline">{{ $t('user.tip_register_account') }}</NuxtLink>
</i18n-t>
</span>
</div>
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy">
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
{{ $t('action.sign_in') }}
</button>
</form>
</template>

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
const { busy, oauth, singleInstanceServer } = useSignIn()
</script>
<template>
<div p8 lg:flex="~ col gap2" hidden>
<p v-if="isHydrated" text-sm>
<i18n-t keypath="user.sign_in_notice_title">
<strong>{{ currentServer }}</strong>
</i18n-t>
</p>
<p text-sm text-secondary>
{{ $t(singleInstanceServer ? 'user.single_instance_sign_in_desc' : 'user.sign_in_desc') }}
</p>
<button
v-if="singleInstanceServer"
flex="~ row" gap-x-2 items-center justify-center btn-solid text-center rounded-3
:disabled="busy"
@click="oauth()"
>
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
{{ $t('action.sign_in') }}
</button>
<button v-else btn-solid rounded-3 text-center mt-2 select-none @click="openSigninDialog()">
{{ $t('action.sign_in') }}
</button>
</div>
</template>

View file

@ -0,0 +1,65 @@
<script setup lang="ts">
import type { UserLogin } from '#shared/types'
const emit = defineEmits<{
(event: 'click'): void
}>()
const all = useUsers()
const { singleInstanceServer, oauth } = useSignIn()
const sorted = computed(() => {
return [
currentUser.value!,
...all.value.filter(account => account.token !== currentUser.value?.token),
].filter(Boolean)
})
const router = useRouter()
function clickUser(user: UserLogin) {
if (user.account.id === currentUser.value?.account.id)
router.push(getAccountRoute(user.account))
else
switchUser(user)
}
function processSignIn() {
if (singleInstanceServer)
oauth()
else
openSigninDialog()
}
</script>
<template>
<div sm:min-w-80 max-w-100vw mxa py2 flex="~ col" @click="emit('click')">
<template v-for="user of sorted" :key="user.id">
<button
flex rounded px4 py3 text-left
hover:bg-active cursor-pointer transition-100
:aria-label="$t('action.switch_account')"
@click="clickUser(user)"
>
<AccountInfo :account="user.account" :hover-card="false" square />
<div flex-auto />
<div v-if="user.token === currentUser?.token" i-ri:check-line text-primary mya text-2xl />
</button>
</template>
<div border="t base" pt2>
<CommonDropdownItem
is="button"
:text="$t('user.add_existing')"
icon="i-ri:user-add-line"
w-full
@click="processSignIn"
/>
<CommonDropdownItem
is="button"
v-if="isHydrated && currentUser"
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
icon="i-ri:logout-box-line rtl-flip"
w-full
@click="signOut"
/>
</div>
</div>
</template>