diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 631d19e..c3fe5ac 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, Suspense, useRef, useCallback } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { useAuth } from '@/lib/auth/context' -import { AUTH_URL } from '@/lib/api/client' +import { AUTH_URL, default as apiRequest } from '@/lib/api/client' import { exchangeAuthCode, setSessionAction } from '@/app/actions/auth' import { authMessageFromErrorType, type AuthErrorType } from '@/lib/utils/authErrors' import { LoadingOverlay } from '@ciphera-net/ui' @@ -23,13 +23,22 @@ function AuthCallbackContent() { if (!code || !codeVerifier) return const result = await exchangeAuthCode(code, codeVerifier, redirectUri) if (result.success && result.user) { - login(result.user) + // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint + try { + const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me') + const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role } + login(merged) + } catch { + login(result.user) + } localStorage.removeItem('oauth_state') localStorage.removeItem('oauth_code_verifier') if (localStorage.getItem('pulse_pending_checkout')) { router.push('/welcome') } else { - router.push('/') + const raw = searchParams.get('returnTo') || '/' + const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/' + router.push(safe) } } else { setError(authMessageFromErrorType(result.error as AuthErrorType)) @@ -51,12 +60,20 @@ function AuthCallbackContent() { const handleDirectTokens = async () => { const result = await setSessionAction(token, refreshToken) if (result.success && result.user) { - login(result.user) + // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint + try { + const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me') + const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role } + login(merged) + } catch { + login(result.user) + } if (typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')) { router.push('/welcome') } else { - const returnTo = searchParams.get('returnTo') || '/' - router.push(returnTo) + const raw = searchParams.get('returnTo') || '/' + const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/' + router.push(safe) } } else { setError(authMessageFromErrorType('invalid')) @@ -91,6 +108,7 @@ function AuthCallbackContent() { const handleRetry = () => { setError(null) + processedRef.current = false setIsRetrying(true) } diff --git a/app/integrations/nextjs/page.tsx b/app/integrations/nextjs/page.tsx new file mode 100644 index 0000000..12ec2c4 --- /dev/null +++ b/app/integrations/nextjs/page.tsx @@ -0,0 +1,129 @@ +'use client' + +import Link from 'next/link' +import { ArrowLeftIcon } from '@ciphera-net/ui' + +export default function NextJsIntegrationPage() { + return ( +
+ {/* * --- ATMOSPHERE (Background) --- */} +
+
+
+
+
+ +
+ + + Back to Integrations + + +
+
+ + + +
+

+ Next.js Integration +

+
+ +
+

+ The best way to add Pulse to your Next.js application is using the built-in next/script component. +

+ +
+ +

Using App Router (Recommended)

+

+ Add the script to your root layout file (usually app/layout.tsx or app/layout.js). +

+ +
+
+ app/layout.tsx +
+
+
+{`import Script from 'next/script'
+
+export default function RootLayout({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  return (
+    
+      
+        
+    
+    My React App
+  
+  
+    
+ +`} +
+
+
+ +

Method 2: Programmatic Injection

+

+ If you need to load the script dynamically (e.g., only in production), you can use a useEffect hook in your main App component. +

+ +
+
+ src/App.tsx +
+
+
+{`import { useEffect } from 'react'
+
+function App() {
+  useEffect(() => {
+    // Only load in production
+    if (process.env.NODE_ENV === 'production') {
+      const script = document.createElement('script')
+      script.defer = true
+      script.setAttribute('data-domain', 'your-site.com')
+      script.src = 'https://pulse.ciphera.net/script.js'
+      document.head.appendChild(script)
+    }
+  }, [])
+
+  return (
+    
+

Hello World

+
+ ) +}`} +
+
+
+
+
+
+ ) +} diff --git a/app/integrations/vue/page.tsx b/app/integrations/vue/page.tsx new file mode 100644 index 0000000..40adb04 --- /dev/null +++ b/app/integrations/vue/page.tsx @@ -0,0 +1,113 @@ +'use client' + +import Link from 'next/link' +import { ArrowLeftIcon } from '@ciphera-net/ui' + +export default function VueIntegrationPage() { + return ( +
+ {/* * --- ATMOSPHERE (Background) --- */} +
+
+
+
+
+ +
+ + + Back to Integrations + + +
+
+ + + + +
+

+ Vue.js Integration +

+
+ +
+

+ Integrating Pulse with Vue.js is straightforward. You can add the script to your index.html file. +

+ +
+ +

Method 1: index.html (Recommended)

+

+ Add the script tag to the <head> section of your index.html file. This works for both Vue 2 and Vue 3 projects created with Vue CLI or Vite. +

+ +
+
+ index.html +
+
+
+{`
+
+  
+    
+    
+    
+    
+    
+    
+    My Vue App
+  
+  
+    
+ + +`} +
+
+
+ +

Method 2: Nuxt.js

+

+ For Nuxt.js applications, you should add the script to your nuxt.config.js or nuxt.config.ts file. +

+ +
+
+ nuxt.config.ts +
+
+
+{`export default defineNuxtConfig({
+  app: {
+    head: {
+      script: [
+        {
+          src: 'https://pulse.ciphera.net/script.js',
+          defer: true,
+          'data-domain': 'your-site.com'
+        }
+      ]
+    }
+  }
+})`}
+              
+
+
+
+
+
+ ) +} diff --git a/app/integrations/wordpress/page.tsx b/app/integrations/wordpress/page.tsx new file mode 100644 index 0000000..a1db8ef --- /dev/null +++ b/app/integrations/wordpress/page.tsx @@ -0,0 +1,81 @@ +'use client' + +import Link from 'next/link' +import { ArrowLeftIcon } from '@ciphera-net/ui' + +export default function WordPressIntegrationPage() { + return ( +
+ {/* * --- ATMOSPHERE (Background) --- */} +
+
+
+
+
+ +
+ + + Back to Integrations + + +
+
+ + + +
+

+ WordPress Integration +

+
+ +
+

+ You can add Pulse to your WordPress site without installing any heavy plugins, or by using a simple code snippet plugin. +

+ +
+ +

Method 1: Using a Plugin (Easiest)

+
    +
  1. Install a plugin like "Insert Headers and Footers" (WPCode).
  2. +
  3. Go to the plugin settings and find the "Scripts in Header" section.
  4. +
  5. Paste the following code snippet:
  6. +
+ +
+
+ Header Script +
+
+
+{``}
+              
+
+
+ +

Method 2: Edit Theme Files (Advanced)

+

+ If you are comfortable editing your theme files, you can add the script directly to your header.php file. +

+
    +
  1. Go to Appearance > Theme File Editor.
  2. +
  3. Select header.php from the right sidebar.
  4. +
  5. Paste the script tag just before the closing </head> tag.
  6. +
+
+
+
+ ) +} diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 7fabff9..ef1e128 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -73,14 +73,6 @@ export default function LayoutContent({ children }: { children: React.ReactNode Features )} - {auth.user && ( - - Tools - - )} } /> diff --git a/app/tools/page.tsx b/app/tools/page.tsx deleted file mode 100644 index e33bf6d..0000000 --- a/app/tools/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import UtmBuilder from '@/components/tools/UtmBuilder' - -export default function ToolsPage() { - return ( -
-

Tools

-
-

UTM Campaign Builder

- -
-
- ) -} diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx index 9199c06..7193206 100644 --- a/app/welcome/page.tsx +++ b/app/welcome/page.tsx @@ -20,6 +20,7 @@ import { createCheckoutSession } from '@/lib/api/billing' import { createSite, type Site } from '@/lib/api/sites' import { setSessionAction } from '@/app/actions/auth' import { useAuth } from '@/lib/auth/context' +import apiRequest from '@/lib/api/client' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { trackWelcomeStepView, @@ -149,7 +150,13 @@ function WelcomeContent() { const { access_token } = await switchContext(org.organization_id) const result = await setSessionAction(access_token) if (result.success && result.user) { - login(result.user) + try { + const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me') + const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role } + login(merged) + } catch { + login(result.user) + } router.refresh() trackWelcomeWorkspaceSelected() setStep(3) @@ -180,7 +187,13 @@ function WelcomeContent() { const { access_token } = await switchContext(org.id) const result = await setSessionAction(access_token) if (result.success && result.user) { - login(result.user) + try { + const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me') + const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role } + login(merged) + } catch { + login(result.user) + } router.refresh() } trackWelcomeWorkspaceCreated(!!(typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout'))) @@ -302,11 +315,11 @@ function WelcomeContent() { }, [step, siteName, siteDomain]) if (orgLoading && step === 2) { - return + return } if (switchingOrgId) { - return + return } if (redirectingCheckout || (planLoading && step === 3)) { @@ -357,50 +370,73 @@ function WelcomeContent() { className={cardClass} > {orgsLoading ? ( -
-

Loading your workspaces...

+
+
+

Loading your organizations...

) : organizations && organizations.length > 0 ? ( <> -
-
- +
+
+
-

+

Choose your organization -

-

- Continue with an existing organization or create a new one. + +

+ Continue with an existing one or create a new organization.

-
- {organizations.map((org) => ( - - ))} +
+ {organizations.map((org, index) => { + const isCurrent = user?.org_id === org.organization_id + const initial = (org.organization_name || 'O').charAt(0).toUpperCase() + return ( + handleSelectOrganization(org)} + disabled={!!switchingOrgId} + initial={{ opacity: 0, y: 8 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: index * 0.04, duration: 0.2 }} + className={`w-full flex items-center gap-3 rounded-xl border px-4 py-3.5 text-left transition-all duration-200 disabled:opacity-60 ${ + isCurrent + ? 'border-brand-orange/60 bg-brand-orange/5 dark:bg-brand-orange/10 shadow-sm' + : 'border-neutral-200 dark:border-neutral-700 bg-neutral-50/80 dark:bg-neutral-800/50 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:border-neutral-300 dark:hover:border-neutral-600 hover:shadow-sm' + }`} + > +
+ {initial} +
+ + {org.organization_name || 'Organization'} + + {isCurrent && ( + Current + )} + +
+ ) + })} +
+
+
- ) : (
@@ -484,7 +520,7 @@ function WelcomeContent() { onChange={(e) => setOrgSlug(e.target.value)} className="w-full" /> -

+

Used in your organization URL.

@@ -651,9 +687,15 @@ function WelcomeContent() { variant="primary" className="flex-1" disabled={siteLoading || !siteName.trim() || !siteDomain.trim()} - isLoading={siteLoading} > - Add site + {siteLoading ? ( + <> + + Adding... + + ) : ( + 'Add site' + )}