fix(settings): lock site context to current URL, rename Workspace to Organization

- Site context is locked to the site from the current URL — no dropdown
  switcher. If not on a site page, defaults to Organization context.
- Renamed "Workspace" to "Organization" in all user-facing text.
- Removed unused CaretDown import and dropdown state.
This commit is contained in:
Usman Baig
2026-03-24 16:52:59 +01:00
parent ea2c47b53f
commit e12a3661fa
3 changed files with 34 additions and 77 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useCallback, useEffect } from 'react' import { useState, useCallback, useEffect } from 'react'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { X, GearSix, Buildings, User, CaretDown } from '@phosphor-icons/react' import { X, GearSix, Buildings, User } from '@phosphor-icons/react'
import { useUnifiedSettings } from '@/lib/unified-settings-context' import { useUnifiedSettings } from '@/lib/unified-settings-context'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { useSite } from '@/lib/swr/dashboard' import { useSite } from '@/lib/swr/dashboard'
@@ -65,28 +65,18 @@ const ACCOUNT_TABS: TabDef[] = [
function ContextSwitcher({ function ContextSwitcher({
active, active,
onChange, onChange,
sites, activeSiteDomain,
activeSiteId,
onSiteChange,
}: { }: {
active: SettingsContext active: SettingsContext
onChange: (ctx: SettingsContext) => void onChange: (ctx: SettingsContext) => void
sites: Site[] activeSiteDomain: string | null
activeSiteId: string | null
onSiteChange: (id: string) => void
}) { }) {
const [siteDropdownOpen, setSiteDropdownOpen] = useState(false)
const activeSite = sites.find(s => s.id === activeSiteId)
return ( return (
<div className="flex items-center gap-1 p-1 bg-neutral-800/50 rounded-xl"> <div className="flex items-center gap-1 p-1 bg-neutral-800/50 rounded-xl">
{/* Site button with dropdown */} {/* Site button — locked to current site, no dropdown */}
<div className="relative"> {activeSiteDomain && (
<button <button
onClick={() => { onClick={() => onChange('site')}
onChange('site')
if (active === 'site') setSiteDropdownOpen(!siteDropdownOpen)
}}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${ className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
active === 'site' active === 'site'
? 'bg-neutral-700 text-white shadow-sm' ? 'bg-neutral-700 text-white shadow-sm'
@@ -94,45 +84,12 @@ function ContextSwitcher({
}`} }`}
> >
<GearSix weight="bold" className="w-4 h-4" /> <GearSix weight="bold" className="w-4 h-4" />
<span className="hidden sm:inline"> <span className="hidden sm:inline">{activeSiteDomain}</span>
{activeSite ? activeSite.domain : 'Site'}
</span>
<CaretDown weight="bold" className="w-3 h-3" />
</button> </button>
)}
<AnimatePresence>
{siteDropdownOpen && active === 'site' && sites.length > 1 && (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }}
className="absolute top-full left-0 mt-1 w-56 rounded-xl bg-neutral-800 border border-neutral-700 shadow-xl z-50 py-1 overflow-hidden"
>
{sites.map(site => (
<button
key={site.id}
onClick={() => {
onSiteChange(site.id)
setSiteDropdownOpen(false)
}}
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
site.id === activeSiteId
? 'bg-brand-orange/10 text-brand-orange'
: 'text-neutral-300 hover:bg-neutral-700/50'
}`}
>
<span className="font-medium">{site.name}</span>
<span className="ml-2 text-neutral-500 text-xs">{site.domain}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
<button <button
onClick={() => { onChange('workspace'); setSiteDropdownOpen(false) }} onClick={() => onChange('workspace')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${ className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
active === 'workspace' active === 'workspace'
? 'bg-neutral-700 text-white shadow-sm' ? 'bg-neutral-700 text-white shadow-sm'
@@ -140,11 +97,11 @@ function ContextSwitcher({
}`} }`}
> >
<Buildings weight="bold" className="w-4 h-4" /> <Buildings weight="bold" className="w-4 h-4" />
<span className="hidden sm:inline">Workspace</span> <span className="hidden sm:inline">Organization</span>
</button> </button>
<button <button
onClick={() => { onChange('account'); setSiteDropdownOpen(false) }} onClick={() => onChange('account')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${ className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
active === 'account' active === 'account'
? 'bg-neutral-700 text-white shadow-sm' ? 'bg-neutral-700 text-white shadow-sm'
@@ -282,26 +239,28 @@ export default function UnifiedSettingsModal() {
} }
}, [isOpen, initTab]) }, [isOpen, initTab])
// Load sites when modal opens // Detect site from URL and load sites list when modal opens
useEffect(() => { useEffect(() => {
if (isOpen && user?.org_id) { if (!isOpen || !user?.org_id) return
listSites().then(data => {
const list = Array.isArray(data) ? data : []
setSites(list)
if (!activeSiteId && list.length > 0) {
setActiveSiteId(list[0].id)
}
}).catch(() => {})
}
}, [isOpen, user?.org_id])
// Try to pick up site from URL // Pick up site ID from URL — this is the only site the user can configure
useEffect(() => { if (typeof window !== 'undefined') {
if (isOpen && typeof window !== 'undefined') {
const match = window.location.pathname.match(/\/sites\/([a-f0-9-]+)/) const match = window.location.pathname.match(/\/sites\/([a-f0-9-]+)/)
if (match) setActiveSiteId(match[1]) if (match) {
setActiveSiteId(match[1])
setContext('site')
} else {
// Not on a site page — default to organization context
setActiveSiteId(null)
if (!initTab?.context) setContext('workspace')
}
} }
}, [isOpen])
// Load sites for domain display
listSites().then(data => {
setSites(Array.isArray(data) ? data : [])
}).catch(() => {})
}, [isOpen, user?.org_id])
// Escape key closes // Escape key closes
useEffect(() => { useEffect(() => {
@@ -363,9 +322,7 @@ export default function UnifiedSettingsModal() {
<ContextSwitcher <ContextSwitcher
active={context} active={context}
onChange={handleContextChange} onChange={handleContextChange}
sites={sites} activeSiteDomain={sites.find(s => s.id === activeSiteId)?.domain ?? null}
activeSiteId={activeSiteId}
onSiteChange={setActiveSiteId}
/> />
{/* Tabs */} {/* Tabs */}

View File

@@ -31,9 +31,9 @@ export default function WorkspaceGeneralTab() {
setSaving(true) setSaving(true)
try { try {
await updateOrganization(user.org_id, name, slug) await updateOrganization(user.org_id, name, slug)
toast.success('Workspace updated') toast.success('Organization updated')
} catch (err) { } catch (err) {
toast.error(getAuthErrorMessage(err as Error) || 'Failed to update workspace') toast.error(getAuthErrorMessage(err as Error) || 'Failed to update organization')
} finally { } finally {
setSaving(false) setSaving(false)
} }
@@ -52,7 +52,7 @@ export default function WorkspaceGeneralTab() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="text-base font-semibold text-white mb-1">General Information</h3> <h3 className="text-base font-semibold text-white mb-1">General Information</h3>
<p className="text-sm text-neutral-400">Basic details about your workspace.</p> <p className="text-sm text-neutral-400">Basic details about your organization.</p>
</div> </div>
<div> <div>

View File

@@ -82,7 +82,7 @@ export default function WorkspaceMembersTab() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-base font-semibold text-white mb-1">Members</h3> <h3 className="text-base font-semibold text-white mb-1">Members</h3>
<p className="text-sm text-neutral-400">{members.length} member{members.length !== 1 ? 's' : ''} in your workspace.</p> <p className="text-sm text-neutral-400">{members.length} member{members.length !== 1 ? 's' : ''} in your organization.</p>
</div> </div>
{canManage && !showInvite && ( {canManage && !showInvite && (
<Button onClick={() => setShowInvite(true)} variant="primary" className="text-sm gap-1.5"> <Button onClick={() => setShowInvite(true)} variant="primary" className="text-sm gap-1.5">