refactor: replace all legacy settings links with unified modal openers
This commit is contained in:
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -401,7 +433,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user