Merge branch 'main' into staging
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user