trypost/resources/js/pages/welcome/Persona.vue
Paulo Castellano b4f61be6ef
Welcome: pre-subscription funnel and member subscription-required screen (#243)
* Rename pre-subscription onboarding funnel to Welcome.

Move the ICP steps to /welcome, drop the social-connect checkout gate, hold unpaid members on a subscription-required screen, and keep legacy /onboarding URLs working until the post-subscription checklist lands.

Closes #237

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop legacy /onboarding ICP URL aliases.

Unfinished users re-enter Welcome via EnsureAccountReady on next login; /onboarding stays free for the post-subscription checklist.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify Welcome PostHog event names.

Use welcome.persona/goals/referral and drop the unused checkout case — begin checkout stays on the frontend as checkout.started / begin_checkout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Slim welcome goal options to match the #204 set.

Drop team_collaboration, automate_api, and track_performance so the goals step stays at nine choices.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Split Welcome AI goals into TryPost AI and MCP assistants.

Rewrite ai_content for in-app generation and add use_mcp so Claude/ChatGPT/Cursor intent is captured separately across locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden Welcome goals gate and drop dead checkout UI.

Treat removed goal values as incomplete so mid-funnel users re-select, remove the unused canCheckout branch, and fix the pt-BR welcome progress label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add password visibility toggle to the login form.

Match the register eye control so users can reveal their password while signing in.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Point the sidebar community link to Discord.

Replace the X stay-updated entry with Join Discord and the trypost.it/discord invite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Rename the sidebar Discord link to Discord community.

Softer label that matches the other support nav items.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix broken Turkish Discord community translation.

An unescaped apostrophe left a parse error in lang/tr/sidebar.php.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 11:34:50 -03:00

169 lines
4.8 KiB
Vue

<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import {
IconBriefcase,
IconBuildingSkyscraper,
IconBuildingStore,
IconCheck,
IconCode,
IconDots,
IconRocket,
IconShoppingBag,
IconSpeakerphone,
IconUser,
} from '@tabler/icons-vue';
import { trans } from 'laravel-vue-i18n';
import type { FunctionalComponent } from 'vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import WelcomeLayout from '@/layouts/WelcomeLayout.vue';
import { store } from '@/routes/app/welcome/persona';
const props = defineProps<{
personas: string[];
selected?: string | null;
}>();
const form = useForm({ persona: props.selected ?? '' });
const personaMeta: Record<
string,
{ icon: FunctionalComponent; iconClass: string; badge: string }
> = {
creator: {
icon: IconUser,
iconClass: 'text-rose-700',
badge: 'bg-rose-100',
},
freelancer: {
icon: IconBriefcase,
iconClass: 'text-amber-700',
badge: 'bg-amber-100',
},
developer: {
icon: IconCode,
iconClass: 'text-cyan-700',
badge: 'bg-cyan-100',
},
startup: {
icon: IconRocket,
iconClass: 'text-violet-700',
badge: 'bg-violet-100',
},
agency: {
icon: IconBuildingSkyscraper,
iconClass: 'text-blue-700',
badge: 'bg-blue-100',
},
small_business: {
icon: IconBuildingStore,
iconClass: 'text-emerald-700',
badge: 'bg-emerald-100',
},
marketer: {
icon: IconSpeakerphone,
iconClass: 'text-fuchsia-700',
badge: 'bg-fuchsia-100',
},
online_store: {
icon: IconShoppingBag,
iconClass: 'text-teal-700',
badge: 'bg-teal-100',
},
other: {
icon: IconDots,
iconClass: 'text-foreground',
badge: 'bg-muted',
},
};
const metaFor = (value: string) =>
personaMeta[value] ?? {
icon: IconDots,
iconClass: 'text-foreground',
badge: 'bg-muted',
};
const personaLabel = (value: string): string =>
trans(`welcome.personas.${value}`);
const select = (value: string): void => {
form.persona = value;
};
const submit = (): void => {
if (!form.persona || form.processing) {
return;
}
form.submit(store());
};
</script>
<template>
<Head :title="$t('welcome.title')" />
<WelcomeLayout
:title="$t('welcome.title')"
:description="$t('welcome.description')"
:step="1"
wide
>
<div class="flex flex-wrap justify-center gap-2.5">
<button
v-for="persona in personas"
:key="persona"
type="button"
:aria-pressed="form.persona === persona"
:data-testid="`welcome-persona-${persona}`"
:class="[
'inline-flex cursor-pointer items-center gap-3 rounded-full border-2 border-foreground py-2.5 ps-2.5 pe-5 text-start shadow-2xs transition-shadow hover:shadow-md',
form.persona === persona ? 'bg-violet-100' : 'bg-card',
]"
@click="select(persona)"
>
<span
:class="[
'inline-flex size-11 shrink-0 items-center justify-center rounded-full border-2 border-foreground shadow-2xs',
metaFor(persona).badge,
]"
>
<component
:is="metaFor(persona).icon"
:class="[metaFor(persona).iconClass, 'size-6']"
stroke-width="2"
/>
</span>
<span
class="text-sm font-bold tracking-tight text-foreground sm:text-base"
>
{{ personaLabel(persona) }}
</span>
<span
v-if="form.persona === persona"
class="inline-flex size-5 shrink-0 items-center justify-center rounded-full border-2 border-foreground bg-foreground"
>
<IconCheck
class="size-3 text-background"
stroke-width="3"
/>
</span>
</button>
</div>
<div class="mx-auto flex w-full max-w-sm flex-col items-center gap-3">
<InputError :message="form.errors.persona" />
<Button
type="button"
size="lg"
class="w-full rounded-full"
:disabled="!form.persona || form.processing"
data-testid="welcome-persona-continue"
@click="submit"
>
{{ $t('welcome.continue') }}
</Button>
</div>
</WelcomeLayout>
</template>