Merge branch 'main' into staging

This commit is contained in:
Usman
2026-02-09 09:58:56 +01:00
committed by GitHub
4 changed files with 70 additions and 46 deletions

View File

@@ -17,7 +17,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode
const isOnline = useOnlineStatus() const isOnline = useOnlineStatus()
const [orgs, setOrgs] = useState<any[]>([]) const [orgs, setOrgs] = useState<any[]>([])
// * Fetch organizations for the header workspace switcher // * Fetch organizations for the header organization switcher
useEffect(() => { useEffect(() => {
if (auth.user) { if (auth.user) {
getUserOrganizations() getUserOrganizations()
@@ -27,7 +27,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode
}, [auth.user]) }, [auth.user])
const handleSwitchOrganization = async (orgId: string | null) => { const handleSwitchOrganization = async (orgId: string | null) => {
if (!orgId) return // Pulse doesn't support personal workspace if (!orgId) return // Pulse doesn't support personal organization context
try { try {
const { access_token } = await switchContext(orgId) const { access_token } = await switchContext(orgId)
await setSessionAction(access_token) await setSessionAction(access_token)

View File

@@ -2,7 +2,7 @@
/** /**
* Guided onboarding wizard for new Pulse users. * Guided onboarding wizard for new Pulse users.
* Steps: Welcome → Workspace (create org) → Plan / trial → First site (optional) → Done. * Steps: Welcome → Organization (create org) → Plan / trial → First site (optional) → Done.
* Supports ?step= in URL for back/refresh. Handles pulse_pending_checkout from pricing. * Supports ?step= in URL for back/refresh. Handles pulse_pending_checkout from pricing.
*/ */
@@ -45,6 +45,7 @@ import {
} from '@ciphera-net/ui' } from '@ciphera-net/ui'
import Link from 'next/link' import Link from 'next/link'
import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock' import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock'
import VerificationModal from '@/components/sites/VerificationModal'
const TOTAL_STEPS = 5 const TOTAL_STEPS = 5
const DEFAULT_ORG_NAME = 'My organization' const DEFAULT_ORG_NAME = 'My organization'
@@ -97,6 +98,7 @@ function WelcomeContent() {
const [siteLoading, setSiteLoading] = useState(false) const [siteLoading, setSiteLoading] = useState(false)
const [siteError, setSiteError] = useState('') const [siteError, setSiteError] = useState('')
const [createdSite, setCreatedSite] = useState<Site | null>(null) const [createdSite, setCreatedSite] = useState<Site | null>(null)
const [showVerificationModal, setShowVerificationModal] = useState(false)
const [redirectingCheckout, setRedirectingCheckout] = useState(false) const [redirectingCheckout, setRedirectingCheckout] = useState(false)
const [hadPendingCheckout, setHadPendingCheckout] = useState<boolean | null>(null) const [hadPendingCheckout, setHadPendingCheckout] = useState<boolean | null>(null)
@@ -176,7 +178,7 @@ function WelcomeContent() {
) )
} }
const handleWorkspaceSubmit = async (e: React.FormEvent) => { const handleOrganizationSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setOrgLoading(true) setOrgLoading(true)
setOrgError('') setOrgError('')
@@ -333,7 +335,7 @@ function WelcomeContent() {
'bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl shadow-sm p-8 max-w-lg mx-auto' 'bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl shadow-sm p-8 max-w-lg mx-auto'
return ( return (
<div className="min-h-[80vh] flex flex-col items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-4 py-12"> <div className="flex-1 flex flex-col items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-4 py-12">
<div className="w-full max-w-lg"> <div className="w-full max-w-lg">
<div <div
className="flex justify-center gap-1.5 mb-8" className="flex justify-center gap-1.5 mb-8"
@@ -473,7 +475,7 @@ function WelcomeContent() {
<button <button
type="button" type="button"
onClick={() => setStep(1)} onClick={() => setStep(1)}
className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6" className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus:outline-none focus:ring-2 focus:ring-brand-orange rounded"
aria-label="Back to welcome" aria-label="Back to welcome"
> >
<ArrowLeftIcon className="h-4 w-4" /> <ArrowLeftIcon className="h-4 w-4" />
@@ -483,14 +485,14 @@ function WelcomeContent() {
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-brand-orange/10 text-brand-orange mb-4"> <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-brand-orange/10 text-brand-orange mb-4">
<BarChartIcon className="h-7 w-7" /> <BarChartIcon className="h-7 w-7" />
</div> </div>
<h2 className="text-xl font-bold text-neutral-900 dark:text-white"> <h1 className="text-xl font-bold text-neutral-900 dark:text-white">
Name your organization Name your organization
</h2> </h1>
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400"> <p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
You can change this later in settings. You can change this later in settings.
</p> </p>
</div> </div>
<form onSubmit={handleWorkspaceSubmit} className="space-y-4"> <form onSubmit={handleOrganizationSubmit} className="space-y-4">
<div> <div>
<label htmlFor="welcome-org-name" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1"> <label htmlFor="welcome-org-name" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
Organization name Organization name
@@ -544,7 +546,7 @@ function WelcomeContent() {
<button <button
type="button" type="button"
onClick={() => setStep(2)} onClick={() => setStep(2)}
className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6" className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus:outline-none focus:ring-2 focus:ring-brand-orange rounded"
aria-label="Back to organization" aria-label="Back to organization"
> >
<ArrowLeftIcon className="h-4 w-4" /> <ArrowLeftIcon className="h-4 w-4" />
@@ -554,9 +556,9 @@ function WelcomeContent() {
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-4"> <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-4">
<CheckCircleIcon className="h-7 w-7" /> <CheckCircleIcon className="h-7 w-7" />
</div> </div>
<h2 className="text-xl font-bold text-neutral-900 dark:text-white"> <h1 className="text-xl font-bold text-neutral-900 dark:text-white">
{showPendingCheckoutInStep3 ? 'Complete your plan' : "You're on the free plan"} {showPendingCheckoutInStep3 ? 'Complete your plan' : "You're on the free plan"}
</h2> </h1>
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400"> <p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
{showPendingCheckoutInStep3 {showPendingCheckoutInStep3
? 'You chose a plan on the pricing page. Continue to add a payment method and start your trial.' ? 'You chose a plan on the pricing page. Continue to add a payment method and start your trial.'
@@ -587,33 +589,32 @@ function WelcomeContent() {
</Button> </Button>
</> </>
) : ( ) : (
<> <Button
<Button variant="primary"
variant="primary" className="w-full sm:w-auto"
className="w-full sm:w-auto" onClick={() => setStep(4)}
onClick={() => setStep(4)} >
> Continue
Continue <ArrowRightIcon className="ml-2 h-4 w-4" />
<ArrowRightIcon className="ml-2 h-4 w-4" /> </Button>
</Button>
<p className="mt-4 text-center">
<Link href="/pricing" className="text-sm text-brand-orange hover:underline">
View pricing
</Link>
</p>
</>
)} )}
</div> </div>
{showPendingCheckoutInStep3 && ( {showPendingCheckoutInStep3 ? (
<p className="mt-4 text-center"> <p className="mt-4 text-center">
<button <button
type="button" type="button"
onClick={() => router.push('/pricing')} onClick={() => router.push('/pricing')}
className="text-sm text-brand-orange hover:underline" className="text-sm text-brand-orange hover:underline focus:outline-none focus:ring-2 focus:ring-brand-orange rounded"
> >
Choose a different plan Choose a different plan
</button> </button>
</p> </p>
) : (
<p className="mt-4 text-center">
<Link href="/pricing" className="text-sm text-brand-orange hover:underline focus:outline-none focus:ring-2 focus:ring-brand-orange rounded">
View pricing
</Link>
</p>
)} )}
</motion.div> </motion.div>
)} )}
@@ -630,7 +631,7 @@ function WelcomeContent() {
<button <button
type="button" type="button"
onClick={() => setStep(3)} onClick={() => setStep(3)}
className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6" className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus:outline-none focus:ring-2 focus:ring-brand-orange rounded"
aria-label="Back to plan" aria-label="Back to plan"
> >
<ArrowLeftIcon className="h-4 w-4" /> <ArrowLeftIcon className="h-4 w-4" />
@@ -640,9 +641,9 @@ function WelcomeContent() {
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-brand-orange/10 text-brand-orange mb-4"> <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-brand-orange/10 text-brand-orange mb-4">
<GlobeIcon className="h-7 w-7" /> <GlobeIcon className="h-7 w-7" />
</div> </div>
<h2 className="text-xl font-bold text-neutral-900 dark:text-white"> <h1 className="text-xl font-bold text-neutral-900 dark:text-white">
Add your first site Add your first site
</h2> </h1>
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400"> <p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
Optional. You can add sites later from the dashboard. Optional. You can add sites later from the dashboard.
</p> </p>
@@ -673,7 +674,7 @@ function WelcomeContent() {
onChange={(e) => setSiteDomain(e.target.value.toLowerCase().trim())} onChange={(e) => setSiteDomain(e.target.value.toLowerCase().trim())}
className="w-full" className="w-full"
/> />
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400"> <p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
Without http:// or https:// Without http:// or https://
</p> </p>
</div> </div>
@@ -723,9 +724,9 @@ function WelcomeContent() {
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-6"> <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-6">
<CheckCircleIcon className="h-7 w-7" /> <CheckCircleIcon className="h-7 w-7" />
</div> </div>
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white"> <h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
You're all set You're all set
</h2> </h1>
<p className="mt-2 text-neutral-600 dark:text-neutral-400"> <p className="mt-2 text-neutral-600 dark:text-neutral-400">
{createdSite {createdSite
? `"${createdSite.name}" is ready. Add the script to your site to start collecting data.` ? `"${createdSite.name}" is ready. Add the script to your site to start collecting data.`
@@ -742,6 +743,21 @@ function WelcomeContent() {
</div> </div>
)} )}
{createdSite && (
<div className="mt-6 flex flex-wrap items-center justify-center gap-2">
<button
type="button"
onClick={() => setShowVerificationModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300 rounded-xl hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all text-sm font-medium focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2"
>
<span className="text-brand-orange">Verify installation</span>
</button>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
Check if your site is sending data correctly.
</p>
</div>
)}
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center"> <div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<Button variant="primary" onClick={goToDashboard} className="min-w-[160px]"> <Button variant="primary" onClick={goToDashboard} className="min-w-[160px]">
Go to dashboard Go to dashboard
@@ -752,6 +768,14 @@ function WelcomeContent() {
</Button> </Button>
)} )}
</div> </div>
{createdSite && (
<VerificationModal
isOpen={showVerificationModal}
onClose={() => setShowVerificationModal(false)}
site={createdSite}
/>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@@ -7,16 +7,16 @@ import { switchContext, OrganizationMember } from '@/lib/api/organization'
import { setSessionAction } from '@/app/actions/auth' import { setSessionAction } from '@/app/actions/auth'
import Link from 'next/link' import Link from 'next/link'
export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) { export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) {
const router = useRouter() const router = useRouter()
const [switching, setSwitching] = useState<string | null>(null) const [switching, setSwitching] = useState<string | null>(null)
const handleSwitch = async (orgId: string | null) => { const handleSwitch = async (orgId: string | null) => {
console.log('Switching to workspace:', orgId) console.log('Switching to organization:', orgId)
setSwitching(orgId || 'personal') setSwitching(orgId || 'personal')
try { try {
// * If orgId is null, we can't switch context via API in the same way if strict mode is on // * If orgId is null, we can't switch context via API in the same way if strict mode is on
// * BUT, Pulse doesn't support personal workspace. // * Pulse doesn't support personal organization context.
// * So we should probably NOT show the "Personal" option in Pulse if strict mode is enforced. // * So we should probably NOT show the "Personal" option in Pulse if strict mode is enforced.
// * However, to match Drop exactly, we might want to show it but have it fail or redirect? // * However, to match Drop exactly, we might want to show it but have it fail or redirect?
// * Let's assume for now we want to match Drop's UI structure. // * Let's assume for now we want to match Drop's UI structure.
@@ -38,7 +38,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
window.location.reload() window.location.reload()
} catch (err) { } catch (err) {
console.error('Failed to switch workspace', err) console.error('Failed to switch organization', err)
setSwitching(null) setSwitching(null)
} }
} }
@@ -46,10 +46,10 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
return ( return (
<div className="border-b border-neutral-100 dark:border-neutral-800 pb-2 mb-2"> <div className="border-b border-neutral-100 dark:border-neutral-800 pb-2 mb-2">
<div className="px-3 py-2 text-xs font-medium text-neutral-500 uppercase tracking-wider"> <div className="px-3 py-2 text-xs font-medium text-neutral-500 uppercase tracking-wider">
Workspaces Organizations
</div> </div>
{/* Personal Workspace - HIDDEN IN PULSE (Strict Mode) */} {/* Personal organization - HIDDEN IN PULSE (Strict Mode) */}
{/* {/*
<button <button
onClick={() => handleSwitch(null)} onClick={() => handleSwitch(null)}
@@ -70,7 +70,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
</button> </button>
*/} */}
{/* Organization Workspaces */} {/* Organization list */}
{orgs.map((org) => ( {orgs.map((org) => (
<button <button
key={org.organization_id} key={org.organization_id}

View File

@@ -237,11 +237,11 @@ export default function OrganizationSettings() {
} }
}, [activeTab, currentOrgId, loadAudit, auditFetchTrigger]) }, [activeTab, currentOrgId, loadAudit, auditFetchTrigger])
// If no org ID, we are in personal workspace, so don't show org settings // If no org ID, we are in personal organization context, so don't show org settings
if (!currentOrgId) { if (!currentOrgId) {
return ( return (
<div className="p-6 text-center text-neutral-500"> <div className="p-6 text-center text-neutral-500">
<p>You are in your Personal Workspace. Switch to an Organization to manage its settings.</p> <p>You are in your personal context. Switch to an Organization to manage its settings.</p>
</div> </div>
) )
} }