Legacy settings removal, performance improvements, modal polish #70
@@ -15,7 +15,6 @@ import { getUserOrganizations, switchContext, type OrganizationMember } from '@/
|
|||||||
import { setSessionAction } from '@/app/actions/auth'
|
import { setSessionAction } from '@/app/actions/auth'
|
||||||
import { LoadingOverlay } from '@ciphera-net/ui'
|
import { LoadingOverlay } from '@ciphera-net/ui'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { SettingsModalProvider } from '@/lib/settings-modal-context'
|
|
||||||
import { UnifiedSettingsProvider, useUnifiedSettings } from '@/lib/unified-settings-context'
|
import { UnifiedSettingsProvider, useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
import UnifiedSettingsModal from '@/components/settings/unified/UnifiedSettingsModal'
|
import UnifiedSettingsModal from '@/components/settings/unified/UnifiedSettingsModal'
|
||||||
|
|
||||||
@@ -164,10 +163,8 @@ function LayoutInner({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
export default function LayoutContent({ children }: { children: React.ReactNode }) {
|
export default function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<SettingsModalProvider>
|
<UnifiedSettingsProvider>
|
||||||
<UnifiedSettingsProvider>
|
<LayoutInner>{children}</LayoutInner>
|
||||||
<LayoutInner>{children}</LayoutInner>
|
</UnifiedSettingsProvider>
|
||||||
</UnifiedSettingsProvider>
|
|
||||||
</SettingsModalProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { formatTimeAgo, getTypeIcon } from '@/lib/utils/notifications'
|
import { formatTimeAgo, getTypeIcon } from '@/lib/utils/notifications'
|
||||||
import { Button, ArrowLeftIcon } from '@ciphera-net/ui'
|
import { Button, ArrowLeftIcon } from '@ciphera-net/ui'
|
||||||
|
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
import { NotificationsListSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
import { NotificationsListSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const PAGE_SIZE = 50
|
|||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||||
const [unreadCount, setUnreadCount] = useState(0)
|
const [unreadCount, setUnreadCount] = useState(0)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -125,9 +127,9 @@ export default function NotificationsPage() {
|
|||||||
<h1 className="text-2xl font-bold text-white mb-2">Notifications</h1>
|
<h1 className="text-2xl font-bold text-white mb-2">Notifications</h1>
|
||||||
<p className="text-sm text-neutral-400 mb-6">
|
<p className="text-sm text-neutral-400 mb-6">
|
||||||
Manage which notifications you receive in{' '}
|
Manage which notifications you receive in{' '}
|
||||||
<Link href="/org-settings?tab=notifications" className="text-brand-orange hover:underline">
|
<button onClick={() => openUnifiedSettings({ context: 'workspace', tab: 'notifications' })} className="text-brand-orange hover:underline cursor-pointer">
|
||||||
Organization Settings → Notifications
|
Organization Settings → Notifications
|
||||||
</Link>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
@@ -141,9 +143,9 @@ export default function NotificationsPage() {
|
|||||||
<p>No notifications yet</p>
|
<p>No notifications yet</p>
|
||||||
<p className="text-sm mt-2">
|
<p className="text-sm mt-2">
|
||||||
Manage which notifications you receive in{' '}
|
Manage which notifications you receive in{' '}
|
||||||
<Link href="/org-settings?tab=notifications" className="text-brand-orange hover:underline">
|
<button onClick={() => openUnifiedSettings({ context: 'workspace', tab: 'notifications' })} className="text-brand-orange hover:underline cursor-pointer">
|
||||||
Organization Settings → Notifications
|
Organization Settings → Notifications
|
||||||
</Link>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,30 +1,43 @@
|
|||||||
import { Suspense } from 'react'
|
'use client'
|
||||||
import OrganizationSettings from '@/components/settings/OrganizationSettings'
|
|
||||||
import { SettingsFormSkeleton } from '@/components/skeletons'
|
|
||||||
|
|
||||||
export const metadata = {
|
import { Suspense, useEffect } from 'react'
|
||||||
title: 'Organization Settings - Pulse',
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
description: 'Manage your organization settings',
|
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
}
|
import { Spinner } from '@ciphera-net/ui'
|
||||||
|
|
||||||
|
function OrgSettingsInner() {
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tab = searchParams.get('tab')
|
||||||
|
|
||||||
|
const tabMap: Record<string, string> = {
|
||||||
|
general: 'general',
|
||||||
|
members: 'members',
|
||||||
|
billing: 'billing',
|
||||||
|
notifications: 'notifications',
|
||||||
|
audit: 'audit',
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedTab = tab ? tabMap[tab] || 'general' : 'general'
|
||||||
|
// Go back to wherever the user came from (not always /)
|
||||||
|
router.back()
|
||||||
|
setTimeout(() => openUnifiedSettings({ context: 'workspace', tab: mappedTab }), 200)
|
||||||
|
}, [searchParams, router, openUnifiedSettings])
|
||||||
|
|
||||||
export default function OrgSettingsPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
<div className="flex items-center justify-center py-24">
|
||||||
<div>
|
<Spinner className="w-6 h-6 text-neutral-500" />
|
||||||
<Suspense fallback={
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div>
|
|
||||||
<div className="h-8 w-56 animate-pulse rounded bg-neutral-100 dark:bg-neutral-800 mb-2" />
|
|
||||||
<div className="h-4 w-80 animate-pulse rounded bg-neutral-100 dark:bg-neutral-800" />
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-6 md:p-8">
|
|
||||||
<SettingsFormSkeleton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<OrganizationSettings />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function OrgSettingsRedirect() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="flex items-center justify-center py-24"><Spinner className="w-6 h-6 text-neutral-500" /></div>}>
|
||||||
|
<OrgSettingsInner />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import { toast } from '@ciphera-net/ui'
|
|||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { getSitesLimitForPlan } from '@/lib/plans'
|
import { getSitesLimitForPlan } from '@/lib/plans'
|
||||||
import { formatDate } from '@/lib/utils/formatDate'
|
import { formatDate } from '@/lib/utils/formatDate'
|
||||||
|
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
|
|
||||||
type SiteStatsMap = Record<string, { stats: Stats }>
|
type SiteStatsMap = Record<string, { stats: Stats }>
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { user, loading: authLoading } = useAuth()
|
const { user, loading: authLoading } = useAuth()
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
const [sites, setSites] = useState<Site[]>([])
|
const [sites, setSites] = useState<Site[]>([])
|
||||||
const [sitesLoading, setSitesLoading] = useState(true)
|
const [sitesLoading, setSitesLoading] = useState(true)
|
||||||
const [siteStats, setSiteStats] = useState<SiteStatsMap>({})
|
const [siteStats, setSiteStats] = useState<SiteStatsMap>({})
|
||||||
@@ -355,9 +357,9 @@ export default function HomePage() {
|
|||||||
)}
|
)}
|
||||||
<div className="mt-2 flex gap-2">
|
<div className="mt-2 flex gap-2">
|
||||||
{subscription.has_payment_method ? (
|
{subscription.has_payment_method ? (
|
||||||
<Link href="/org-settings?tab=billing" className="text-sm font-medium text-brand-orange hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:rounded">
|
<button onClick={() => openUnifiedSettings({ context: 'workspace', tab: 'billing' })} className="text-sm font-medium text-brand-orange hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:rounded cursor-pointer">
|
||||||
Manage billing
|
Manage billing
|
||||||
</Link>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Link href="/pricing" className="text-sm font-medium text-brand-orange hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:rounded">
|
<Link href="/pricing" className="text-sm font-medium text-brand-orange hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:rounded">
|
||||||
Upgrade
|
Upgrade
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
import * as Flags from 'country-flag-icons/react/3x2'
|
import * as Flags from 'country-flag-icons/react/3x2'
|
||||||
|
|
||||||
const DottedMap = dynamic(() => import('@/components/dashboard/DottedMap'), { ssr: false })
|
const DottedMap = dynamic(() => import('@/components/dashboard/DottedMap'), { ssr: false })
|
||||||
@@ -115,6 +115,8 @@ export default function CDNPage() {
|
|||||||
const [period, setPeriod] = useState('7')
|
const [period, setPeriod] = useState('7')
|
||||||
const [dateRange, setDateRange] = useState(() => getDateRange(7))
|
const [dateRange, setDateRange] = useState(() => getDateRange(7))
|
||||||
|
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
|
|
||||||
// Data fetching
|
// Data fetching
|
||||||
const { data: bunnyStatus } = useBunnyStatus(siteId)
|
const { data: bunnyStatus } = useBunnyStatus(siteId)
|
||||||
const { data: dashboard } = useDashboard(siteId, dateRange.start, dateRange.end)
|
const { data: dashboard } = useDashboard(siteId, dateRange.start, dateRange.end)
|
||||||
@@ -183,13 +185,13 @@ export default function CDNPage() {
|
|||||||
<p className="text-sm text-neutral-400 max-w-md mb-6">
|
<p className="text-sm text-neutral-400 max-w-md mb-6">
|
||||||
Monitor your CDN performance including bandwidth usage, cache hit rates, request volumes, and geographic distribution.
|
Monitor your CDN performance including bandwidth usage, cache hit rates, request volumes, and geographic distribution.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<button
|
||||||
href={`/sites/${siteId}/settings?tab=integrations`}
|
onClick={() => openUnifiedSettings({ context: 'site', tab: 'integrations' })}
|
||||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-brand-orange hover:bg-brand-orange/90 text-white text-sm font-medium transition-colors"
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-brand-orange hover:bg-brand-orange/90 text-white text-sm font-medium transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Connect in Settings
|
Connect in Settings
|
||||||
<ArrowSquareOut size={16} weight="bold" />
|
<ArrowSquareOut size={16} weight="bold" />
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import dynamic from 'next/dynamic'
|
|||||||
import { DashboardSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
import { DashboardSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
||||||
import FilterBar from '@/components/dashboard/FilterBar'
|
import FilterBar from '@/components/dashboard/FilterBar'
|
||||||
import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
|
import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
|
||||||
import Chart from '@/components/dashboard/Chart'
|
const Chart = dynamic(() => import('@/components/dashboard/Chart'), { ssr: false })
|
||||||
import ContentStats from '@/components/dashboard/ContentStats'
|
import ContentStats from '@/components/dashboard/ContentStats'
|
||||||
import TopReferrers from '@/components/dashboard/TopReferrers'
|
import TopReferrers from '@/components/dashboard/TopReferrers'
|
||||||
import Locations from '@/components/dashboard/Locations'
|
import Locations from '@/components/dashboard/Locations'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
import { Select, DatePicker } from '@ciphera-net/ui'
|
import { Select, DatePicker } from '@ciphera-net/ui'
|
||||||
import { getDateRange, formatDate, getThisWeekRange, getThisMonthRange } from '@/lib/utils/dateRanges'
|
import { getDateRange, formatDate, getThisWeekRange, getThisMonthRange } from '@/lib/utils/dateRanges'
|
||||||
import { CaretDown, CaretUp, MagnifyingGlass, ArrowSquareOut } from '@phosphor-icons/react'
|
import { CaretDown, CaretUp, MagnifyingGlass, ArrowSquareOut } from '@phosphor-icons/react'
|
||||||
@@ -36,6 +36,7 @@ const PAGE_SIZE = 50
|
|||||||
export default function SearchConsolePage() {
|
export default function SearchConsolePage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const siteId = params.id as string
|
const siteId = params.id as string
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
const [period, setPeriod] = useState('28')
|
const [period, setPeriod] = useState('28')
|
||||||
@@ -172,13 +173,13 @@ export default function SearchConsolePage() {
|
|||||||
<p className="text-sm text-neutral-400 max-w-md mb-6">
|
<p className="text-sm text-neutral-400 max-w-md mb-6">
|
||||||
See how your site performs in Google Search. View top queries, pages, click-through rates, and average position data.
|
See how your site performs in Google Search. View top queries, pages, click-through rates, and average position data.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<button
|
||||||
href={`/sites/${siteId}/settings?tab=integrations`}
|
onClick={() => openUnifiedSettings({ context: 'site', tab: 'integrations' })}
|
||||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-brand-orange hover:bg-brand-orange/90 text-white text-sm font-medium transition-colors"
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-brand-orange hover:bg-brand-orange/90 text-white text-sm font-medium transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Connect in Settings
|
Connect in Settings
|
||||||
<ArrowSquareOut size={16} weight="bold" />
|
<ArrowSquareOut size={16} weight="bold" />
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -431,6 +431,7 @@ export default function Chart({
|
|||||||
xDataKey="dateObj"
|
xDataKey="dateObj"
|
||||||
aspectRatio="2.5 / 1"
|
aspectRatio="2.5 / 1"
|
||||||
margin={{ top: 20, right: 20, bottom: 40, left: 50 }}
|
margin={{ top: 20, right: 20, bottom: 40, left: 50 }}
|
||||||
|
animationDuration={400}
|
||||||
>
|
>
|
||||||
<VisxGrid horizontal vertical={false} stroke="var(--chart-grid)" strokeDasharray="4,4" />
|
<VisxGrid horizontal vertical={false} stroke="var(--chart-grid)" strokeDasharray="4,4" />
|
||||||
<VisxArea
|
<VisxArea
|
||||||
|
|||||||
@@ -327,6 +327,38 @@ function NavLink({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Settings Button (opens unified modal instead of navigating) ─────
|
||||||
|
|
||||||
|
function SettingsButton({
|
||||||
|
item, collapsed, onClick,
|
||||||
|
}: {
|
||||||
|
item: NavItem; collapsed: boolean; onClick?: () => void
|
||||||
|
}) {
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group/nav">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
openUnifiedSettings({ context: 'site', tab: 'general' })
|
||||||
|
onClick?.()
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium overflow-hidden transition-all duration-150 text-neutral-400 hover:text-white hover:bg-white/[0.06] hover:translate-x-0.5 w-full cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
||||||
|
<item.icon className="w-[18px] h-[18px]" weight="regular" />
|
||||||
|
</span>
|
||||||
|
<Label collapsed={collapsed}>{item.label}</Label>
|
||||||
|
</button>
|
||||||
|
{collapsed && (
|
||||||
|
<span className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 rounded-md bg-neutral-800 text-white text-xs whitespace-nowrap opacity-0 group-hover/nav:opacity-100 transition-opacity duration-150 delay-150 z-50">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Sidebar Content ────────────────────────────────────────
|
// ─── Sidebar Content ────────────────────────────────────────
|
||||||
|
|
||||||
interface SidebarContentProps {
|
interface SidebarContentProps {
|
||||||
@@ -347,12 +379,13 @@ interface SidebarContentProps {
|
|||||||
orgs: OrganizationMember[]
|
orgs: OrganizationMember[]
|
||||||
onSwitchOrganization: (orgId: string | null) => Promise<void>
|
onSwitchOrganization: (orgId: string | null) => Promise<void>
|
||||||
openSettings: () => void
|
openSettings: () => void
|
||||||
|
openOrgSettings: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarContent({
|
function SidebarContent({
|
||||||
isMobile, collapsed, siteId, sites, canEdit, pendingHref,
|
isMobile, collapsed, siteId, sites, canEdit, pendingHref,
|
||||||
onNavigate, onMobileClose, onExpand, onCollapse, onToggle,
|
onNavigate, onMobileClose, onExpand, onCollapse, onToggle,
|
||||||
wasCollapsed, pickerOpenCallbackRef, auth, orgs, onSwitchOrganization, openSettings,
|
wasCollapsed, pickerOpenCallbackRef, auth, orgs, onSwitchOrganization, openSettings, openOrgSettings,
|
||||||
}: SidebarContentProps) {
|
}: SidebarContentProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const c = isMobile ? false : collapsed
|
const c = isMobile ? false : collapsed
|
||||||
@@ -401,7 +434,7 @@ function SidebarContent({
|
|||||||
<NavLink key={item.label} item={item} siteId={siteId} collapsed={c} onClick={isMobile ? onMobileClose : undefined} pendingHref={pendingHref} onNavigate={onNavigate} />
|
<NavLink key={item.label} item={item} siteId={siteId} collapsed={c} onClick={isMobile ? onMobileClose : undefined} pendingHref={pendingHref} onNavigate={onNavigate} />
|
||||||
))}
|
))}
|
||||||
{group.label === 'Infrastructure' && canEdit && (
|
{group.label === 'Infrastructure' && canEdit && (
|
||||||
<NavLink item={SETTINGS_ITEM} siteId={siteId} collapsed={c} onClick={isMobile ? onMobileClose : undefined} pendingHref={pendingHref} onNavigate={onNavigate} />
|
<SettingsButton item={SETTINGS_ITEM} collapsed={c} onClick={isMobile ? onMobileClose : undefined} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,6 +465,7 @@ function SidebarContent({
|
|||||||
onCreateOrganization={() => router.push('/onboarding')}
|
onCreateOrganization={() => router.push('/onboarding')}
|
||||||
allowPersonalOrganization={false}
|
allowPersonalOrganization={false}
|
||||||
onOpenSettings={openSettings}
|
onOpenSettings={openSettings}
|
||||||
|
onOpenOrgSettings={openOrgSettings}
|
||||||
compact
|
compact
|
||||||
anchor="right"
|
anchor="right"
|
||||||
>
|
>
|
||||||
@@ -533,6 +567,7 @@ export default function Sidebar({
|
|||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
onSwitchOrganization={handleSwitchOrganization}
|
onSwitchOrganization={handleSwitchOrganization}
|
||||||
openSettings={() => openUnifiedSettings({ context: 'account', tab: 'profile' })}
|
openSettings={() => openUnifiedSettings({ context: 'account', tab: 'profile' })}
|
||||||
|
openOrgSettings={() => openUnifiedSettings({ context: 'workspace', tab: 'general' })}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -576,6 +611,7 @@ export default function Sidebar({
|
|||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
onSwitchOrganization={handleSwitchOrganization}
|
onSwitchOrganization={handleSwitchOrganization}
|
||||||
openSettings={() => openUnifiedSettings({ context: 'account', tab: 'profile' })}
|
openSettings={() => openUnifiedSettings({ context: 'account', tab: 'profile' })}
|
||||||
|
openOrgSettings={() => openUnifiedSettings({ context: 'workspace', tab: 'general' })}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Link from 'next/link'
|
|||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
|
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
|
||||||
import { useAuth } from '@/lib/auth/context'
|
|
||||||
|
|
||||||
interface SiteNavProps {
|
interface SiteNavProps {
|
||||||
siteId: string
|
siteId: string
|
||||||
@@ -13,8 +12,6 @@ interface SiteNavProps {
|
|||||||
export default function SiteNav({ siteId }: SiteNavProps) {
|
export default function SiteNav({ siteId }: SiteNavProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const handleTabKeyDown = useTabListKeyboard()
|
const handleTabKeyDown = useTabListKeyboard()
|
||||||
const { user } = useAuth()
|
|
||||||
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: 'Dashboard', href: `/sites/${siteId}` },
|
{ label: 'Dashboard', href: `/sites/${siteId}` },
|
||||||
@@ -24,7 +21,6 @@ export default function SiteNav({ siteId }: SiteNavProps) {
|
|||||||
{ label: 'Search', href: `/sites/${siteId}/search` },
|
{ label: 'Search', href: `/sites/${siteId}/search` },
|
||||||
{ label: 'CDN', href: `/sites/${siteId}/cdn` },
|
{ label: 'CDN', href: `/sites/${siteId}/cdn` },
|
||||||
{ label: 'Uptime', href: `/sites/${siteId}/uptime` },
|
{ label: 'Uptime', href: `/sites/${siteId}/uptime` },
|
||||||
...(canEdit ? [{ label: 'Settings', href: `/sites/${siteId}/settings` }] : []),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const isActive = (href: string) => {
|
const isActive = (href: string) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { listNotifications, markNotificationRead, markAllNotificationsRead, type
|
|||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { formatTimeAgo, getTypeIcon } from '@/lib/utils/notifications'
|
import { formatTimeAgo, getTypeIcon } from '@/lib/utils/notifications'
|
||||||
import { SettingsIcon } from '@ciphera-net/ui'
|
import { SettingsIcon } from '@ciphera-net/ui'
|
||||||
|
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
||||||
import { SkeletonLine, SkeletonCircle } from '@/components/skeletons'
|
import { SkeletonLine, SkeletonCircle } from '@/components/skeletons'
|
||||||
|
|
||||||
// * Bell icon (simple SVG, no extra deps)
|
// * Bell icon (simple SVG, no extra deps)
|
||||||
@@ -58,6 +59,7 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
const panelRef = useRef<HTMLDivElement>(null)
|
const panelRef = useRef<HTMLDivElement>(null)
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
const [fixedPos, setFixedPos] = useState<{ left: number; top?: number; bottom?: number } | null>(null)
|
const [fixedPos, setFixedPos] = useState<{ left: number; top?: number; bottom?: number } | null>(null)
|
||||||
|
const { openUnifiedSettings } = useUnifiedSettings()
|
||||||
|
|
||||||
const updatePosition = useCallback(() => {
|
const updatePosition = useCallback(() => {
|
||||||
if (anchor === 'right' && buttonRef.current) {
|
if (anchor === 'right' && buttonRef.current) {
|
||||||
@@ -319,14 +321,16 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
>
|
>
|
||||||
View all
|
View all
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<button
|
||||||
href="/org-settings?tab=notifications"
|
onClick={() => {
|
||||||
onClick={() => setOpen(false)}
|
setOpen(false)
|
||||||
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
|
openUnifiedSettings({ context: 'workspace', tab: 'notifications' })
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-4 h-4" aria-hidden="true" />
|
<SettingsIcon className="w-4 h-4" aria-hidden="true" />
|
||||||
Manage settings
|
Manage settings
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,124 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { SettingsModal, type SettingsSection } from '@ciphera-net/ui'
|
|
||||||
import { UserIcon, LockIcon, BellIcon, ChevronRightIcon } from '@ciphera-net/ui'
|
|
||||||
import { NotificationToggleList, type NotificationOption } from '@ciphera-net/ui'
|
|
||||||
import ProfileSettings from '@/components/settings/ProfileSettings'
|
|
||||||
import TrustedDevicesCard from '@/components/settings/TrustedDevicesCard'
|
|
||||||
import SecurityActivityCard from '@/components/settings/SecurityActivityCard'
|
|
||||||
import { useSettingsModal } from '@/lib/settings-modal-context'
|
|
||||||
import { useAuth } from '@/lib/auth/context'
|
|
||||||
import { updateUserPreferences } from '@/lib/api/user'
|
|
||||||
|
|
||||||
// --- Security Alerts ---
|
|
||||||
|
|
||||||
const SECURITY_ALERT_OPTIONS: NotificationOption[] = [
|
|
||||||
{ key: 'login_alerts', label: 'Login Activity', description: 'New device sign-ins and suspicious login attempts.' },
|
|
||||||
{ key: 'password_alerts', label: 'Password Changes', description: 'Password changes and session revocations.' },
|
|
||||||
{ key: 'two_factor_alerts', label: 'Two-Factor Authentication', description: '2FA enabled/disabled and recovery code changes.' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function SecurityAlertsCard() {
|
|
||||||
const { user } = useAuth()
|
|
||||||
const [emailNotifications, setEmailNotifications] = useState<Record<string, boolean>>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user?.preferences?.email_notifications) {
|
|
||||||
setEmailNotifications(user.preferences.email_notifications)
|
|
||||||
} else {
|
|
||||||
const defaults = SECURITY_ALERT_OPTIONS.reduce((acc, option) => ({
|
|
||||||
...acc,
|
|
||||||
[option.key]: true
|
|
||||||
}), {} as Record<string, boolean>)
|
|
||||||
setEmailNotifications(defaults)
|
|
||||||
}
|
|
||||||
}, [user])
|
|
||||||
|
|
||||||
const handleToggle = async (key: string) => {
|
|
||||||
const newState = {
|
|
||||||
...emailNotifications,
|
|
||||||
[key]: !emailNotifications[key]
|
|
||||||
}
|
|
||||||
setEmailNotifications(newState)
|
|
||||||
try {
|
|
||||||
await updateUserPreferences({
|
|
||||||
email_notifications: newState as { new_file_received: boolean; file_downloaded: boolean; login_alerts: boolean; password_alerts: boolean; two_factor_alerts: boolean }
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
setEmailNotifications(prev => ({
|
|
||||||
...prev,
|
|
||||||
[key]: !prev[key]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationToggleList
|
|
||||||
title="Security Alerts"
|
|
||||||
description="Choose which security events trigger email alerts"
|
|
||||||
icon={<BellIcon className="w-5 h-5 text-brand-orange" />}
|
|
||||||
options={SECURITY_ALERT_OPTIONS}
|
|
||||||
values={emailNotifications}
|
|
||||||
onToggle={handleToggle}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Notification Center Placeholder ---
|
|
||||||
|
|
||||||
function NotificationCenterPlaceholder() {
|
|
||||||
return (
|
|
||||||
<div className="text-center max-w-md mx-auto py-8">
|
|
||||||
<BellIcon className="w-12 h-12 text-neutral-300 mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-medium text-white mb-2">Notification Center</h3>
|
|
||||||
<p className="text-sm text-neutral-500 mb-4">View and manage all your notifications in one place.</p>
|
|
||||||
<Link href="/notifications" className="inline-flex items-center gap-2 px-4 py-2 bg-brand-orange text-white rounded-lg hover:bg-brand-orange/90 transition-colors">
|
|
||||||
Open Notification Center
|
|
||||||
<ChevronRightIcon className="w-4 h-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main Wrapper ---
|
|
||||||
|
|
||||||
export default function SettingsModalWrapper() {
|
|
||||||
const { isOpen, closeSettings } = useSettingsModal()
|
|
||||||
|
|
||||||
const sections: SettingsSection[] = [
|
|
||||||
{
|
|
||||||
id: 'pulse',
|
|
||||||
label: 'Account',
|
|
||||||
icon: UserIcon,
|
|
||||||
defaultExpanded: true,
|
|
||||||
items: [
|
|
||||||
{ id: 'profile', label: 'Profile', content: <ProfileSettings activeTab="profile" borderless hideDangerZone /> },
|
|
||||||
{ id: 'security', label: 'Security', content: <ProfileSettings activeTab="security" borderless /> },
|
|
||||||
{ id: 'preferences', label: 'Preferences', content: <ProfileSettings activeTab="preferences" borderless /> },
|
|
||||||
{ id: 'danger-zone', label: 'Danger Zone', content: <ProfileSettings activeTab="danger-zone" borderless /> },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'security-section',
|
|
||||||
label: 'Security',
|
|
||||||
icon: LockIcon,
|
|
||||||
items: [
|
|
||||||
{ id: 'devices', label: 'Trusted Devices', content: <TrustedDevicesCard /> },
|
|
||||||
{ id: 'activity', label: 'Security Activity', content: <SecurityActivityCard /> },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notifications',
|
|
||||||
label: 'Notifications',
|
|
||||||
icon: BellIcon,
|
|
||||||
items: [
|
|
||||||
{ id: 'security-alerts', label: 'Security Alerts', content: <SecurityAlertsCard /> },
|
|
||||||
{ id: 'center', label: 'Notification Center', content: <NotificationCenterPlaceholder /> },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return <SettingsModal open={isOpen} onClose={closeSettings} sections={sections} />
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { X, GearSix, Buildings, User } from '@phosphor-icons/react'
|
import { X, GearSix, Buildings, User } from '@phosphor-icons/react'
|
||||||
import { Button } from '@ciphera-net/ui'
|
import { Button, Spinner } from '@ciphera-net/ui'
|
||||||
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'
|
||||||
import { listSites, type Site } from '@/lib/api/sites'
|
import { listSites, type Site } from '@/lib/api/sites'
|
||||||
|
|
||||||
// Tab content components — Site
|
// Lazy-load tab components — only loaded when the tab is first rendered
|
||||||
import SiteGeneralTab from './tabs/SiteGeneralTab'
|
const tabLoader = () => <div className="flex items-center justify-center py-12"><Spinner className="w-6 h-6 text-neutral-500" /></div>
|
||||||
import SiteGoalsTab from './tabs/SiteGoalsTab'
|
const SiteGeneralTab = dynamic(() => import('./tabs/SiteGeneralTab'), { loading: tabLoader })
|
||||||
import SiteVisibilityTab from './tabs/SiteVisibilityTab'
|
const SiteGoalsTab = dynamic(() => import('./tabs/SiteGoalsTab'), { loading: tabLoader })
|
||||||
import SitePrivacyTab from './tabs/SitePrivacyTab'
|
const SiteVisibilityTab = dynamic(() => import('./tabs/SiteVisibilityTab'), { loading: tabLoader })
|
||||||
import SiteBotSpamTab from './tabs/SiteBotSpamTab'
|
const SitePrivacyTab = dynamic(() => import('./tabs/SitePrivacyTab'), { loading: tabLoader })
|
||||||
import SiteReportsTab from './tabs/SiteReportsTab'
|
const SiteBotSpamTab = dynamic(() => import('./tabs/SiteBotSpamTab'), { loading: tabLoader })
|
||||||
import SiteIntegrationsTab from './tabs/SiteIntegrationsTab'
|
const SiteReportsTab = dynamic(() => import('./tabs/SiteReportsTab'), { loading: tabLoader })
|
||||||
// Tab content components — Workspace
|
const SiteIntegrationsTab = dynamic(() => import('./tabs/SiteIntegrationsTab'), { loading: tabLoader })
|
||||||
import WorkspaceGeneralTab from './tabs/WorkspaceGeneralTab'
|
const WorkspaceGeneralTab = dynamic(() => import('./tabs/WorkspaceGeneralTab'), { loading: tabLoader })
|
||||||
import WorkspaceBillingTab from './tabs/WorkspaceBillingTab'
|
const WorkspaceBillingTab = dynamic(() => import('./tabs/WorkspaceBillingTab'), { loading: tabLoader })
|
||||||
import WorkspaceMembersTab from './tabs/WorkspaceMembersTab'
|
const WorkspaceMembersTab = dynamic(() => import('./tabs/WorkspaceMembersTab'), { loading: tabLoader })
|
||||||
import WorkspaceNotificationsTab from './tabs/WorkspaceNotificationsTab'
|
const WorkspaceNotificationsTab = dynamic(() => import('./tabs/WorkspaceNotificationsTab'), { loading: tabLoader })
|
||||||
import WorkspaceAuditTab from './tabs/WorkspaceAuditTab'
|
const WorkspaceAuditTab = dynamic(() => import('./tabs/WorkspaceAuditTab'), { loading: tabLoader })
|
||||||
// Tab content components — Account
|
const AccountProfileTab = dynamic(() => import('./tabs/AccountProfileTab'), { loading: tabLoader })
|
||||||
import AccountProfileTab from './tabs/AccountProfileTab'
|
const AccountSecurityTab = dynamic(() => import('./tabs/AccountSecurityTab'), { loading: tabLoader })
|
||||||
import AccountSecurityTab from './tabs/AccountSecurityTab'
|
const AccountDevicesTab = dynamic(() => import('./tabs/AccountDevicesTab'), { loading: tabLoader })
|
||||||
import AccountDevicesTab from './tabs/AccountDevicesTab'
|
|
||||||
|
|
||||||
// ─── Types ──────────────────────────────────────────────────────
|
// ─── Types ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -72,46 +72,35 @@ function ContextSwitcher({
|
|||||||
onChange: (ctx: SettingsContext) => void
|
onChange: (ctx: SettingsContext) => void
|
||||||
activeSiteDomain: string | null
|
activeSiteDomain: string | null
|
||||||
}) {
|
}) {
|
||||||
|
const items: { id: SettingsContext; icon: React.ReactNode; label: string; visible: boolean }[] = [
|
||||||
|
{ id: 'site', icon: <GearSix weight="bold" className="w-4 h-4" />, label: activeSiteDomain || '', visible: !!activeSiteDomain },
|
||||||
|
{ id: 'workspace', icon: <Buildings weight="bold" className="w-4 h-4" />, label: 'Organization', visible: true },
|
||||||
|
{ id: 'account', icon: <User weight="bold" className="w-4 h-4" />, label: 'Account', visible: true },
|
||||||
|
]
|
||||||
|
|
||||||
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 — locked to current site, no dropdown */}
|
{items.filter(i => i.visible).map(item => (
|
||||||
{activeSiteDomain && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('site')}
|
key={item.id}
|
||||||
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
|
onClick={() => onChange(item.id)}
|
||||||
active === 'site'
|
className={`relative flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
|
||||||
? 'bg-neutral-700 text-white shadow-sm'
|
active === item.id ? 'text-white' : 'text-neutral-400 hover:text-white'
|
||||||
: 'text-neutral-400 hover:text-white'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<GearSix weight="bold" className="w-4 h-4" />
|
{active === item.id && (
|
||||||
<span className="hidden sm:inline">{activeSiteDomain}</span>
|
<motion.div
|
||||||
|
layoutId="context-switcher-bg"
|
||||||
|
className="absolute inset-0 bg-neutral-700 rounded-lg shadow-sm"
|
||||||
|
transition={{ type: 'spring', bounce: 0.15, duration: 0.35 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="relative z-10 flex items-center gap-2">
|
||||||
|
{item.icon}
|
||||||
|
<span className="hidden sm:inline">{item.label}</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
))}
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => onChange('workspace')}
|
|
||||||
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
|
|
||||||
active === 'workspace'
|
|
||||||
? 'bg-neutral-700 text-white shadow-sm'
|
|
||||||
: 'text-neutral-400 hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Buildings weight="bold" className="w-4 h-4" />
|
|
||||||
<span className="hidden sm:inline">Organization</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => onChange('account')}
|
|
||||||
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
|
|
||||||
active === 'account'
|
|
||||||
? 'bg-neutral-700 text-white shadow-sm'
|
|
||||||
: 'text-neutral-400 hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<User weight="bold" className="w-4 h-4" />
|
|
||||||
<span className="hidden sm:inline">Account</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -318,16 +307,20 @@ export default function UnifiedSettingsModal() {
|
|||||||
const match = window.location.pathname.match(/\/sites\/([a-f0-9-]+)/)
|
const match = window.location.pathname.match(/\/sites\/([a-f0-9-]+)/)
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveSiteId(match[1])
|
setActiveSiteId(match[1])
|
||||||
setContext('site')
|
// Only default to site context if no specific context was requested
|
||||||
|
if (!initTab?.context) setContext('site')
|
||||||
} else {
|
} else {
|
||||||
setActiveSiteId(null)
|
setActiveSiteId(null)
|
||||||
if (!initTab?.context) setContext('workspace')
|
if (!initTab?.context) setContext('workspace')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listSites().then(data => {
|
// Only fetch sites if we don't have them yet
|
||||||
setSites(Array.isArray(data) ? data : [])
|
if (sites.length === 0) {
|
||||||
}).catch(() => {})
|
listSites().then(data => {
|
||||||
|
setSites(Array.isArray(data) ? data : [])
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
}, [isOpen, user?.org_id])
|
}, [isOpen, user?.org_id])
|
||||||
|
|
||||||
// Global keyboard shortcuts: `,` toggles settings, Escape closes
|
// Global keyboard shortcuts: `,` toggles settings, Escape closes
|
||||||
@@ -393,9 +386,10 @@ export default function UnifiedSettingsModal() {
|
|||||||
? 'opacity-0 pointer-events-none transition-opacity duration-150'
|
? 'opacity-0 pointer-events-none transition-opacity duration-150'
|
||||||
: 'opacity-0 pointer-events-none invisible'
|
: 'opacity-0 pointer-events-none invisible'
|
||||||
}`}
|
}`}
|
||||||
|
onClick={handleBackdropClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-3xl h-[85vh] bg-neutral-900/65 backdrop-blur-3xl backdrop-saturate-150 supports-[backdrop-filter]:bg-neutral-900/60 border border-white/[0.08] rounded-2xl shadow-xl shadow-black/20 flex flex-col overflow-hidden"
|
className="relative w-full max-w-4xl h-[90vh] bg-neutral-900/65 backdrop-blur-3xl backdrop-saturate-150 supports-[backdrop-filter]:bg-neutral-900/60 border border-white/[0.08] rounded-2xl shadow-xl shadow-black/20 flex flex-col overflow-hidden"
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Content animates in/out */}
|
{/* Content animates in/out */}
|
||||||
@@ -435,18 +429,9 @@ export default function UnifiedSettingsModal() {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
<AnimatePresence mode="wait">
|
<div key={`${context}-${activeTab}`} className="p-6">
|
||||||
<motion.div
|
<TabContent context={context} activeTab={activeTab} siteId={activeSiteId} onDirtyChange={handleDirtyChange} onRegisterSave={handleRegisterSave} />
|
||||||
key={`${context}-${activeTab}`}
|
</div>
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.12 }}
|
|
||||||
className="p-6"
|
|
||||||
>
|
|
||||||
<TabContent context={context} activeTab={activeTab} siteId={activeSiteId} onDirtyChange={handleDirtyChange} onRegisterSave={handleRegisterSave} />
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save bar */}
|
{/* Save bar */}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { createContext, useContext, useState, useCallback } from 'react'
|
|
||||||
|
|
||||||
interface SettingsModalContextType {
|
|
||||||
isOpen: boolean
|
|
||||||
openSettings: () => void
|
|
||||||
closeSettings: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsModalContext = createContext<SettingsModalContextType>({
|
|
||||||
isOpen: false,
|
|
||||||
openSettings: () => {},
|
|
||||||
closeSettings: () => {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export function SettingsModalProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const openSettings = useCallback(() => setIsOpen(true), [])
|
|
||||||
const closeSettings = useCallback(() => setIsOpen(false), [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsModalContext.Provider value={{ isOpen, openSettings, closeSettings }}>
|
|
||||||
{children}
|
|
||||||
</SettingsModalContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSettingsModal() {
|
|
||||||
return useContext(SettingsModalContext)
|
|
||||||
}
|
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.15.0-alpha",
|
"version": "0.15.0-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.3.2",
|
"@ciphera-net/ui": "^0.3.3",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@icons-pack/react-simple-icons": "^13.13.0",
|
"@icons-pack/react-simple-icons": "^13.13.0",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
@@ -1682,9 +1682,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ciphera-net/ui": {
|
"node_modules/@ciphera-net/ui": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.3.2/f2271906ba6b3827dfc9598f6aed95bff20ae2a0",
|
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.3.3/e67893f5cadc93c0dcb0b9987b9d370dce0d073b",
|
||||||
"integrity": "sha512-bi8PANCpl27bCfIiHBmgyPFOKkTYgLHw/wOeUe9COAFHJj8MJGEj0rrXMa8L9Bf9IChw+MlITAojrklc1P8Kkw==",
|
"integrity": "sha512-Z5A8qj0ZuMMJq72ZREknxDlL1LciUoirmQmkHmp4H2Vl/5ZWELrvdm65V11hJuc2TZpXnCBxtWa2UoKi/SVOIw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.3.2",
|
"@ciphera-net/ui": "^0.3.3",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@icons-pack/react-simple-icons": "^13.13.0",
|
"@icons-pack/react-simple-icons": "^13.13.0",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
|
|||||||
Reference in New Issue
Block a user