feat: add organization onboarding
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { mdiAccountArrowRightOutline, mdiDomainPlus, mdiEmailOutline } from '@mdi/js';
|
||||
import { useOrganizationStore } from '@/features/organizations/stores/organizationStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const organizationStore = useOrganizationStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
|
||||
const createForm = reactive({
|
||||
name: '',
|
||||
});
|
||||
const accessForm = reactive({
|
||||
targetType: 'organization',
|
||||
targetName: '',
|
||||
adminEmail: '',
|
||||
note: '',
|
||||
});
|
||||
const createError = ref(null);
|
||||
const requestStatus = ref(null);
|
||||
|
||||
const accessTypeOptions = computed(() => [
|
||||
{ title: t('organizationOnboarding.request.types.organization'), value: 'organization' },
|
||||
{ title: t('organizationOnboarding.request.types.workspace'), value: 'workspace' },
|
||||
]);
|
||||
|
||||
async function createOrganization() {
|
||||
if (organizationStore.isCreating) {
|
||||
return;
|
||||
}
|
||||
|
||||
createError.value = null;
|
||||
const name = createForm.name.trim();
|
||||
|
||||
if (!name) {
|
||||
createError.value = t('organizationOnboarding.create.errors.required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await organizationStore.createOrganization({ name });
|
||||
await Promise.all([
|
||||
organizationStore.fetchOrganizations(),
|
||||
workspaceStore.fetchWorkspaces(),
|
||||
]);
|
||||
await router.push({ name: 'workspace-create' });
|
||||
} catch (error) {
|
||||
createError.value = t('organizationOnboarding.create.errors.failed');
|
||||
}
|
||||
}
|
||||
|
||||
function requestAccess() {
|
||||
requestStatus.value = null;
|
||||
|
||||
if (!accessForm.adminEmail.trim() || !accessForm.targetName.trim()) {
|
||||
requestStatus.value = t('organizationOnboarding.request.errors.required');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = t('organizationOnboarding.request.emailSubject', {
|
||||
target: accessForm.targetName.trim(),
|
||||
});
|
||||
const body = t('organizationOnboarding.request.emailBody', {
|
||||
targetType: t(`organizationOnboarding.request.types.${accessForm.targetType}`),
|
||||
target: accessForm.targetName.trim(),
|
||||
note: accessForm.note.trim() || t('organizationOnboarding.request.noNote'),
|
||||
});
|
||||
|
||||
window.location.href = `mailto:${encodeURIComponent(accessForm.adminEmail.trim())}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||
requestStatus.value = t('organizationOnboarding.request.sent');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="onboarding-page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">{{ t('organizationOnboarding.eyebrow') }}</div>
|
||||
<h1>{{ t('organizationOnboarding.title') }}</h1>
|
||||
<p>{{ t('organizationOnboarding.description') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="onboarding-grid">
|
||||
<article class="panel">
|
||||
<div class="panel-header">
|
||||
<v-icon :icon="mdiDomainPlus" />
|
||||
<div>
|
||||
<strong>{{ t('organizationOnboarding.create.title') }}</strong>
|
||||
<span>{{ t('organizationOnboarding.create.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="createError"
|
||||
class="page-message error"
|
||||
>
|
||||
{{ createError }}
|
||||
</div>
|
||||
|
||||
<v-form
|
||||
class="form-stack"
|
||||
@submit.prevent="createOrganization"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="createForm.name"
|
||||
:label="t('organizationOnboarding.create.fields.name')"
|
||||
:placeholder="t('organizationOnboarding.create.fields.namePlaceholder')"
|
||||
:disabled="organizationStore.isCreating"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:loading="organizationStore.isCreating"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
{{ t('organizationOnboarding.create.action') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-header">
|
||||
<v-icon :icon="mdiAccountArrowRightOutline" />
|
||||
<div>
|
||||
<strong>{{ t('organizationOnboarding.request.title') }}</strong>
|
||||
<span>{{ t('organizationOnboarding.request.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="requestStatus"
|
||||
class="page-message"
|
||||
>
|
||||
{{ requestStatus }}
|
||||
</div>
|
||||
|
||||
<v-form
|
||||
class="form-stack"
|
||||
@submit.prevent="requestAccess"
|
||||
>
|
||||
<v-select
|
||||
v-model="accessForm.targetType"
|
||||
:items="accessTypeOptions"
|
||||
:label="t('organizationOnboarding.request.fields.type')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="accessForm.targetName"
|
||||
:label="t('organizationOnboarding.request.fields.name')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="accessForm.adminEmail"
|
||||
:label="t('organizationOnboarding.request.fields.adminEmail')"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-textarea
|
||||
v-model="accessForm.note"
|
||||
:label="t('organizationOnboarding.request.fields.note')"
|
||||
rows="3"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
type="submit"
|
||||
:prepend-icon="mdiEmailOutline"
|
||||
>
|
||||
{{ t('organizationOnboarding.request.action') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
.onboarding-page {
|
||||
@apply mx-auto flex w-full max-w-6xl flex-col gap-6 px-5 py-8 md:px-8;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.panel {
|
||||
@apply rounded-lg border;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.hero {
|
||||
@apply p-6 md:p-8;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
@apply text-xs font-bold uppercase tracking-[0.22em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
@apply mt-2 text-3xl font-black md:text-4xl;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.panel-header span {
|
||||
@apply mt-2 text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.onboarding-grid {
|
||||
@apply grid gap-4 lg:grid-cols-2;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@apply flex flex-col gap-4 p-5;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@apply flex gap-3;
|
||||
}
|
||||
|
||||
.panel-header .v-icon {
|
||||
@apply mt-1;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.panel-header strong {
|
||||
@apply block text-xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.form-stack {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.page-message {
|
||||
@apply rounded-lg border p-3 text-sm font-semibold;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(23, 32, 51, 0.04);
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.page-message.error {
|
||||
border-color: rgba(220, 38, 38, 0.24);
|
||||
color: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user