feat: complete onboarding flow

This commit is contained in:
Zoe
2026-10-08 14:04:29 -05:00
parent fae05ebc4c
commit 61f9bb74b4
16 changed files with 684 additions and 297 deletions
+14 -29
View File
@@ -1,5 +1,7 @@
<script setup lang="ts">
import type { RegistrationMode } from '~~/drizzle/schema';
import { DialogType } from '~/composables/useDialog';
import { timeLeft } from '~/utils/time';
type InstanceSettings = {
registration: RegistrationMode;
@@ -25,6 +27,7 @@ const REGISTRATION_OPTIONS: { value: RegistrationMode; label: string; descriptio
];
const { isAdmin } = useAuth();
const { openDialog } = useDialog();
if (!isAdmin.value) {
await navigateTo('/settings/profile');
@@ -43,8 +46,6 @@ type Invite = {
createdAt: string;
};
const DAY_MS = 24 * 60 * 60 * 1000;
const { data: inviteData, refresh: loadInvites } = await useFetch<Invite[]>('/api/admin/invites', {
key: 'admin_invites',
});
@@ -89,9 +90,7 @@ const copiedId = ref<string | null>(null);
const invitePath = (invite: Invite) => `/auth/register?invite=${invite.token}`;
const describeInvite = (invite: Invite) => {
const days = Math.max(1, Math.ceil((new Date(invite.expiresAt).getTime() - Date.now()) / DAY_MS));
return `${invite.uses} of ${invite.maxUses} ${invite.maxUses === 1 ? 'use' : 'uses'} · expires in ${days} ${days === 1 ? 'day' : 'days'}`;
return `${invite.uses} of ${invite.maxUses} ${invite.maxUses === 1 ? 'use' : 'uses'} · expires in ${timeLeft(invite.expiresAt)}`;
};
const copyInvite = async (invite: Invite) => {
@@ -107,21 +106,12 @@ const copyInvite = async (invite: Invite) => {
}
};
const createInvite = async () => {
if (inviteBusy.value) return;
inviteBusy.value = true;
inviteError.value = '';
try {
await $fetch('/api/admin/invites', { method: 'POST' });
// The dialog asks how long the link lasts and for how many people, and shows
// the link. It may be closed after the link was made, so always reload.
const createInvite = () => {
openDialog(DialogType.CreateInvite, async () => {
await loadInvites();
} catch (err) {
console.error('Failed to create invite:', err);
inviteError.value = 'The invite link could not be created. Try again.';
}
inviteBusy.value = false;
});
};
const revokeInvite = async (invite: Invite) => {
@@ -202,16 +192,11 @@ const revokeInvite = async (invite: Invite) => {
<p v-if="inviteError" role="alert" class="text-sm text-red-400">{{ inviteError }}</p>
<div class="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<button type="button" :disabled="inviteBusy" @click="createInvite"
class="flex items-center gap-1.5 h-8 px-3 text-sm rounded-md bg-[var(--bg-container)] @hover:bg-[var(--color-hover)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200">
<span class="i-mynaui-plus text-4" aria-hidden="true"></span>
New invite link
</button>
<span class="text-xs text-[var(--text-secondary)]">
Each link lets one person register and expires after 7 days.
</span>
</div>
<button type="button" @click="createInvite"
class="self-start flex items-center gap-1.5 h-8 px-3 text-sm rounded-md bg-[var(--bg-container)] @hover:bg-[var(--color-hover)] transition-colors duration-200">
<span class="i-mynaui-plus text-4" aria-hidden="true"></span>
New invite link
</button>
</template>
</section>
</div>