feat: Implement user onboarding, subscription management, and refactor social integrations with new UI components and mail templates.
This commit is contained in:
parent
7c00c3387e
commit
d284a14c75
3 changed files with 148 additions and 11 deletions
108
resources/js/pages/accounts/FacebookPageSelect.vue
Normal file
108
resources/js/pages/accounts/FacebookPageSelect.vue
Normal 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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue