feat: Implement user onboarding, subscription management, and refactor social integrations with new UI components and mail templates.

This commit is contained in:
Paulo Castellano 2026-01-16 23:54:30 -03:00
parent 7c00c3387e
commit d284a14c75
3 changed files with 148 additions and 11 deletions

View file

@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Facebook, Check } from 'lucide-vue-next';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { select as selectFacebookPage } from '@/routes/social/facebook';
interface Page {
id: string;
name: string;
username: string | null;
picture: string | null;
access_token: string;
}
interface Workspace {
id: string;
name: string;
}
interface Props {
workspace: Workspace;
pages: Page[];
error?: string;
}
defineProps<Props>();
const formRef = ref<HTMLFormElement | null>(null);
const selectedPageId = ref<string | null>(null);
const handleSelectPage = (page: Page) => {
selectedPageId.value = page.id;
setTimeout(() => formRef.value?.submit(), 0);
};
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? '';
</script>
<template>
<PopupLayout title="Select Facebook Page">
<!-- Hidden form for regular POST submission -->
<form ref="formRef" :action="selectFacebookPage.url()" method="POST" class="hidden">
<input type="hidden" name="_token" :value="csrfToken" />
<input type="hidden" name="page_id" :value="selectedPageId" />
</form>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3">
<img src="/images/accounts/facebook.png" alt="Facebook" class="h-10 w-10" />
<div>
<h1 class="text-xl font-bold tracking-tight">Select Facebook Page</h1>
<p class="text-sm text-muted-foreground">Choose which page you want to connect</p>
</div>
</div>
<Alert v-if="error" variant="destructive">
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<div v-if="pages.length === 0 && !error" class="text-center py-12">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-muted">
<Facebook class="h-7 w-7 text-muted-foreground" />
</div>
<h3 class="mt-4 text-lg font-semibold">No pages found</h3>
<p class="mt-1 text-sm text-muted-foreground">
You are not an admin of any Facebook page.
</p>
</div>
<div v-else class="grid gap-3">
<button
v-for="page in pages"
:key="page.id"
@click="handleSelectPage(page)"
class="group relative overflow-hidden rounded-lg border bg-card p-4 text-left transition-all hover:border-primary hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
<div class="flex items-center gap-4">
<Avatar class="h-12 w-12 rounded-lg">
<AvatarImage v-if="page.picture" :src="page.picture" class="object-cover" />
<AvatarFallback class="rounded-lg bg-blue-100 dark:bg-blue-900">
<Facebook class="h-6 w-6 text-blue-600 dark:text-blue-400" />
</AvatarFallback>
</Avatar>
<div class="flex-1 min-w-0">
<h3 class="font-semibold truncate group-hover:text-primary transition-colors">
{{ page.name }}
</h3>
<p v-if="page.username" class="text-sm text-muted-foreground truncate">
facebook.com/{{ page.username }}
</p>
<p v-else class="text-sm text-muted-foreground">Facebook Page</p>
</div>
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground"
>
<Check class="h-4 w-4" />
</div>
</div>
</div>
</button>
</div>
</div>
</PopupLayout>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Building2, Check } from 'lucide-vue-next';
import PopupLayout from '@/layouts/PopupLayout.vue';
@ -27,18 +27,29 @@ interface Props {
defineProps<Props>();
const formRef = ref<HTMLFormElement | null>(null);
const selectedOrg = ref<Organization | null>(null);
const handleSelectPage = (org: Organization) => {
router.post(selectLinkedInPage.url(), {
organization_id: org.id,
organization_name: org.name,
organization_vanity_name: org.vanity_name,
organization_logo: org.logo,
});
selectedOrg.value = org;
// Submit via regular form to get Blade response
setTimeout(() => formRef.value?.submit(), 0);
};
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? '';
</script>
<template>
<PopupLayout title="Select LinkedIn Page">
<!-- Hidden form for regular POST submission -->
<form ref="formRef" :action="selectLinkedInPage.url()" method="POST" class="hidden">
<input type="hidden" name="_token" :value="csrfToken" />
<input type="hidden" name="organization_id" :value="selectedOrg?.id" />
<input type="hidden" name="organization_name" :value="selectedOrg?.name" />
<input type="hidden" name="organization_vanity_name" :value="selectedOrg?.vanity_name ?? ''" />
<input type="hidden" name="organization_logo" :value="selectedOrg?.logo ?? ''" />
</form>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3">
<img src="/images/accounts/linkedin.png" alt="LinkedIn" class="h-10 w-10" />

View file

@ -47,7 +47,7 @@
};
// Try to notify the parent window
if (window.opener) {
if (window.opener && !window.opener.closed) {
try {
window.opener.postMessage({
type: 'social-oauth-callback',
@ -61,10 +61,28 @@
}
}
// Close this window after a short delay
// Try to close immediately and with delay
function closeWindow() {
try {
window.open('', '_self', '');
window.close();
} catch (e) {}
// Fallback: try again
try {
window.close();
} catch (e) {}
}
// Close after a short delay
setTimeout(closeWindow, 1000);
// If still open after 2 seconds, show manual close message
setTimeout(function() {
window.close();
}, 1500);
if (!window.closed) {
document.querySelector('.submessage').textContent = 'You can close this window now.';
}
}, 2000);
})();
</script>
</body>