142 lines
6.1 KiB
Vue
142 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
const MAX_USES = 1000;
|
|
|
|
const DURATIONS = [
|
|
{ hours: 1, label: '1 hour' },
|
|
{ hours: 24, label: '1 day' },
|
|
{ hours: 24 * 7, label: '7 days' },
|
|
{ hours: 24 * 30, label: '30 days' },
|
|
];
|
|
|
|
const emit = defineEmits(['confirm', 'cancel']);
|
|
|
|
const hours = ref(24 * 7);
|
|
const maxUses = ref(1);
|
|
|
|
const busy = ref(false);
|
|
const error = ref('');
|
|
|
|
// set once the link exists, the dialog then shows it instead of the options
|
|
const link = ref('');
|
|
const copied = ref(false);
|
|
|
|
const usesValid = computed(() => Number.isInteger(maxUses.value) && maxUses.value >= 1 && maxUses.value <= MAX_USES);
|
|
|
|
const copyLink = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(link.value);
|
|
copied.value = true;
|
|
setTimeout(() => {
|
|
copied.value = false;
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy invite link:', err);
|
|
}
|
|
};
|
|
|
|
const submit = async () => {
|
|
if (busy.value || !usesValid.value) return;
|
|
|
|
busy.value = true;
|
|
error.value = '';
|
|
|
|
try {
|
|
const invite = await $fetch<{ token: string }>('/api/admin/invites', {
|
|
method: 'POST',
|
|
body: {
|
|
maxUses: maxUses.value,
|
|
expiresInHours: hours.value,
|
|
},
|
|
});
|
|
|
|
link.value = `${window.location.origin}/auth/register?invite=${invite.token}`;
|
|
} catch (err) {
|
|
console.error('Failed to create invite:', err);
|
|
error.value = 'The invite link could not be created. Try again.';
|
|
}
|
|
|
|
busy.value = false;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<form class="flex flex-col gap-4.5 p-6 w-screen md:w-130 max-w-full h-full md:h-auto overflow-y-auto"
|
|
aria-labelledby="create-invite-title" @submit.prevent="submit">
|
|
<header class="flex flex-col gap-1">
|
|
<h2 id="create-invite-title" class="text-lg font-semibold">
|
|
{{ link ? 'Invite link created' : 'New invite link' }}
|
|
</h2>
|
|
<p class="text-sm text-[var(--text-secondary)]">
|
|
{{ link
|
|
? 'Send it to whoever should join. You can copy it again or revoke it under Instance.'
|
|
: 'Anyone with the link can create an account until it runs out.' }}
|
|
</p>
|
|
</header>
|
|
|
|
<template v-if="link">
|
|
<div class="flex items-center rounded-md bg-[var(--bg-container)]">
|
|
<input type="text" readonly :value="link" aria-label="Invite link" @focus="($event.target as HTMLInputElement).select()"
|
|
class="flex-1 min-w-0 h-9 px-2.5 text-[13px] font-mono bg-transparent outline-none" />
|
|
<button type="button" :aria-label="copied ? 'Copied' : 'Copy invite link'" @click="copyLink"
|
|
class="flex items-center justify-center w-9 h-9 flex-shrink-0 text-[var(--text-secondary)] @hover:text-[var(--text-primary)]">
|
|
<span class="text-4.5" :class="copied ? 'i-mynaui-check' : 'i-mynaui-copy'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<footer class="flex justify-end mt-auto">
|
|
<button type="button"
|
|
class="flex items-center h-10 px-4 rounded-md font-semibold bg-[var(--color-accent)] text-[var(--color-accent-text)]"
|
|
@click="emit('confirm', true)">
|
|
Done
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<fieldset class="flex flex-col">
|
|
<legend class="mb-1.5 text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
|
Valid for
|
|
</legend>
|
|
<div class="flex gap-0.5 p-0.5 rounded-lg bg-[var(--bg-container)] self-start">
|
|
<button v-for="d in DURATIONS" :key="d.hours" type="button" :aria-pressed="hours === d.hours"
|
|
:disabled="busy" @click="hours = d.hours"
|
|
class="flex items-center h-7.5 px-3 text-sm rounded-md disabled:cursor-not-allowed transition-colors duration-200"
|
|
:class="hours === d.hours
|
|
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] font-medium'
|
|
: 'text-[var(--text-secondary)] enabled:@hover:bg-[var(--color-hover)]'">
|
|
{{ d.label }}
|
|
</button>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div class="flex flex-col gap-1.5">
|
|
<label for="create-invite-uses"
|
|
class="text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
|
Number of uses
|
|
</label>
|
|
<input id="create-invite-uses" v-model.number="maxUses" type="number" required min="1"
|
|
:max="MAX_USES" step="1" :disabled="busy"
|
|
class="w-28 h-9 px-2.5 text-sm rounded-md bg-[var(--bg-container)]" />
|
|
<p class="text-xs leading-normal text-[var(--text-secondary)]">
|
|
How many people can register with this link, up to {{ MAX_USES }}.
|
|
</p>
|
|
</div>
|
|
|
|
<p v-if="error" role="alert" class="text-sm text-red-400">{{ error }}</p>
|
|
|
|
<footer class="flex justify-between gap-2 mt-auto">
|
|
<button type="button" :disabled="busy"
|
|
class="flex items-center h-10 px-4 rounded-md border border-[var(--color-border)] text-[var(--text-secondary)] @hover:bg-[var(--color-hover)] disabled:opacity-50 disabled:cursor-not-allowed transition-[background-color] duration-200"
|
|
@click="emit('cancel')">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" :disabled="busy || !usesValid"
|
|
class="flex items-center gap-2 h-10 px-4 rounded-md font-semibold bg-[var(--color-accent)] text-[var(--color-accent-text)] disabled:opacity-40 disabled:cursor-not-allowed">
|
|
<span v-if="busy" class="i-svg-spinners-180-ring-with-bg text-4"></span>
|
|
Create link
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
</form>
|
|
</template>
|