trypost/resources/js/pages/onboarding/Account.vue
Paulo Castellano ded1c998ec feat: redesign billing, onboarding, sidebar, and settings architecture
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
  API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
  -> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
  trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
  sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
  activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
  notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
2026-04-15 00:33:38 -03:00

146 lines
5.1 KiB
Vue

<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { IconCheck } from '@tabler/icons-vue';
import { trans } from 'laravel-vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { storeAccount } from '@/actions/App/Http/Controllers/App/OnboardingController';
import { Button } from '@/components/ui/button';
import AuthLayout from '@/layouts/AuthLayout.vue';
import { role } from '@/routes/app/onboarding';
export interface SocialAccount {
id: string;
platform: string;
username: string;
display_name: string;
status: 'connected' | 'disconnected' | 'token_expired' | null;
}
export interface Platform {
value: string;
label: string;
connected: boolean;
account: SocialAccount | null;
}
interface Props {
platforms: Platform[];
hasWorkspace: boolean;
}
const props = defineProps<Props>();
const isSubmitting = ref(false);
const connectedCount = computed(() =>
props.platforms.filter((p) => p.connected).length,
);
const getPlatformLogo = (platform: string): string => {
const logos: Record<string, string> = {
'linkedin': '/images/accounts/linkedin.png',
'linkedin-page': '/images/accounts/linkedin.png',
'x': '/images/accounts/x.png',
'tiktok': '/images/accounts/tiktok.png',
'instagram': '/images/accounts/instagram.png',
'facebook': '/images/accounts/facebook.png',
'youtube': '/images/accounts/youtube.png',
'threads': '/images/accounts/threads.png',
'bluesky': '/images/accounts/bluesky.png',
'pinterest': '/images/accounts/pinterest.png',
'mastodon': '/images/accounts/mastodon.png',
};
return logos[platform] || '/images/accounts/linkedin.png';
};
const openOAuthPopup = (platformValue: string) => {
const url = `/connect/${platformValue}`;
const width = 600;
const height = 700;
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2;
window.open(
url,
'oauth-popup',
`width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`,
);
};
const handleOAuthMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type !== 'social-oauth-callback') return;
router.reload();
};
onMounted(() => window.addEventListener('message', handleOAuthMessage));
onUnmounted(() => window.removeEventListener('message', handleOAuthMessage));
const submit = () => {
isSubmitting.value = true;
router.post(storeAccount.url());
};
</script>
<template>
<Head :title="$t('onboarding.connect.page_title')" />
<AuthLayout
:title="$t('onboarding.connect.title')"
:description="$t('onboarding.connect.description')"
>
<div v-if="hasWorkspace" class="space-y-4">
<div class="space-y-1.5">
<div
v-for="platform in platforms"
:key="platform.value"
class="flex items-center gap-3 rounded-lg border px-3 py-2.5 transition-colors"
:class="platform.connected ? 'border-green-500/30 bg-green-50/50 dark:bg-green-950/20' : ''"
>
<img
:src="getPlatformLogo(platform.value)"
:alt="platform.label"
class="size-7 rounded object-contain"
/>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium leading-tight">{{ platform.label }}</p>
<p v-if="platform.connected && platform.account" class="truncate text-xs text-muted-foreground">
@{{ platform.account.username || platform.account.display_name }}
</p>
</div>
<div v-if="platform.connected" class="flex size-5 shrink-0 items-center justify-center rounded-full bg-green-500 text-white">
<IconCheck class="size-3" />
</div>
<Button
v-else
variant="outline"
size="sm"
class="h-7 shrink-0 text-xs"
@click="openOAuthPopup(platform.value)"
>
{{ trans('accounts.connect') }}
</Button>
</div>
</div>
<Button
v-if="connectedCount > 0"
class="w-full"
:disabled="isSubmitting"
@click="submit"
>
{{ $t('onboarding.connect.submit') }}
</Button>
</div>
<div v-else class="flex flex-col items-center gap-4 py-8">
<p class="text-muted-foreground">
{{ $t('onboarding.connect.error') }}
</p>
<Button variant="outline" @click="router.visit(role.url())">
{{ $t('onboarding.connect.go_back') }}
</Button>
</div>
</AuthLayout>
</template>