Merge pull request #11 from ciphera-net/staging

[PULSE-43] Design system standardization and branding alignment
This commit is contained in:
Usman
2026-02-05 18:18:07 +01:00
committed by GitHub
33 changed files with 635 additions and 219 deletions

View File

@@ -1,29 +1,239 @@
'use client'
import Link from 'next/link'
import React from 'react'
import Image from 'next/image'
import { GithubIcon, TwitterIcon } from '@ciphera-net/ui'
import SwissFlagIcon from './SwissFlagIcon'
interface FooterProps {
LinkComponent?: any
appName?: string
isAuthenticated?: boolean
}
export function Footer({ LinkComponent = Link, appName = 'Pulse' }: FooterProps) {
const Component = LinkComponent
const footerLinks = {
products: [
{ name: 'Drop', href: 'https://drop.ciphera.net', external: true },
{ name: 'Pulse', href: 'https://pulse.ciphera.net', external: true },
{ name: 'Ciphera Auth', href: 'https://ciphera.net/products#auth', external: true },
{ name: 'Ciphera Captcha', href: 'https://ciphera.net/products#captcha', external: true },
{ name: 'Ciphera Relay', href: 'https://ciphera.net/products#relay', external: true },
],
company: [
{ name: 'About', href: '/about', external: false },
{ name: 'Pricing', href: '/pricing', external: false },
{ name: 'Contact', href: 'https://ciphera.net/contact', external: true },
],
resources: [
{ name: 'Installation', href: '/installation', external: false },
{ name: 'Integrations', href: '/integrations', external: false },
{ name: 'Documentation', href: 'https://docs.ciphera.net', external: true },
{ name: 'Status', href: 'https://status.ciphera.net', external: true },
{ name: 'GitHub', href: 'https://github.com/ciphera-net', external: true },
],
legal: [
{ name: 'Privacy Policy', href: 'https://ciphera.net/#privacy', external: true },
{ name: 'Terms of Service', href: 'https://ciphera.net/#terms', external: true },
],
}
export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticated = false }: FooterProps) {
const Component = LinkComponent
const year = new Date().getFullYear()
// * Simple footer for authenticated users
if (isAuthenticated) {
return (
<footer className="w-full py-8 mt-auto border-t border-neutral-100 dark:border-neutral-800 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-sm text-neutral-500 dark:text-neutral-400">
© 2024-{year} Ciphera. All rights reserved.
</div>
<div className="flex gap-6 text-sm font-medium text-neutral-600 dark:text-neutral-300">
<Component href="/about" className="hover:text-brand-orange transition-colors">
Why {appName}
</Component>
<Component href="/pricing" className="hover:text-brand-orange transition-colors">
Pricing
</Component>
<Component href="/faq" className="hover:text-brand-orange transition-colors">
FAQ
</Component>
</div>
</div>
</div>
</footer>
)
}
// * Comprehensive footer for unauthenticated users
return (
<footer className="border-t border-neutral-200 dark:border-neutral-800 mt-auto bg-white dark:bg-neutral-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-sm text-neutral-500 dark:text-neutral-400">
© {new Date().getFullYear()} Ciphera. All rights reserved.
<footer className="w-full mt-auto border-t border-neutral-100 dark:border-neutral-800 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm">
<div className="mx-auto max-w-6xl px-4 sm:px-6 py-12 lg:py-16">
{/* * Main footer content */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6 sm:gap-8 lg:gap-12">
{/* * Brand column */}
<div className="col-span-1 sm:col-span-2 md:col-span-4 lg:col-span-1 lg:pr-8">
<Link href="/" className="flex items-center gap-3 mb-4 group">
<Image
src="/pulse_icon_no_margins.png"
alt="Pulse privacy-first analytics logo"
width={36}
height={36}
loading="lazy"
className="w-9 h-9 object-contain group-hover:scale-105 transition-transform duration-300"
/>
<span className="text-xl font-bold text-neutral-900 dark:text-white group-hover:text-brand-orange transition-colors duration-300">
Pulse
</span>
</Link>
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4 leading-relaxed">
Simple analytics for privacy-conscious apps.
</p>
<div className="inline-flex items-center gap-2.5 text-sm text-neutral-600 dark:text-neutral-400 mb-4">
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-neutral-100 dark:bg-neutral-800 shrink-0 overflow-hidden ring-1 ring-neutral-200 dark:ring-neutral-700" aria-hidden>
<SwissFlagIcon className="w-5 h-5" />
</span>
<span>Swiss infrastructure</span>
</div>
<div className="flex items-center gap-3">
<a
href="https://github.com/ciphera-net"
target="_blank"
rel="noopener noreferrer"
className="w-9 h-9 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
aria-label="GitHub"
>
<GithubIcon className="w-5 h-5" />
</a>
<a
href="https://x.com/cipheranet"
target="_blank"
rel="noopener noreferrer"
className="w-9 h-9 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
aria-label="X (Twitter)"
>
<TwitterIcon className="w-5 h-5" />
</a>
</div>
</div>
<div className="flex gap-6 text-sm font-medium text-neutral-600 dark:text-neutral-300">
<Component href="/about" className="hover:text-brand-orange transition-colors">
Why {appName}
</Component>
<Component href="/faq" className="hover:text-brand-orange transition-colors">
FAQ
</Component>
{/* * Products */}
<div>
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Products</h4>
<ul className="space-y-3">
{footerLinks.products.map((link) => (
<li key={link.name}>
{link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</a>
) : (
<Component
href={link.href}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</Component>
)}
</li>
))}
</ul>
</div>
{/* * Company */}
<div>
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Company</h4>
<ul className="space-y-3">
{footerLinks.company.map((link) => (
<li key={link.name}>
{link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</a>
) : (
<Component
href={link.href}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</Component>
)}
</li>
))}
</ul>
</div>
{/* * Resources */}
<div>
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Resources</h4>
<ul className="space-y-3">
{footerLinks.resources.map((link) => (
<li key={link.name}>
{link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</a>
) : (
<Component
href={link.href}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</Component>
)}
</li>
))}
</ul>
</div>
{/* * Legal */}
<div>
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Legal</h4>
<ul className="space-y-3">
{footerLinks.legal.map((link) => (
<li key={link.name}>
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
{link.name}
</a>
</li>
))}
</ul>
</div>
</div>
{/* * Divider */}
<div className="h-px w-full bg-gradient-to-r from-transparent via-neutral-200 dark:via-neutral-800 to-transparent my-8" />
{/* * Bottom bar */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
© 2024-{year} Ciphera. All rights reserved.
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Where Privacy Still Exists
</p>
</div>
</div>
</footer>

View File

@@ -62,7 +62,7 @@ export default function PasswordInput({
onBlur={onBlur}
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
className={`w-full pl-11 pr-12 py-3 border rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900
className={`w-full pl-11 pr-12 py-3 border rounded-lg bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900
transition-all duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed dark:text-white
${error
? 'border-red-300 dark:border-red-800 focus:border-red-500 focus:ring-4 focus:ring-red-500/10'

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import { motion } from 'framer-motion'
import { Button, CheckCircleIcon } from '@ciphera-net/ui'
import { useAuth } from '@/lib/auth/context'
import { initiateOAuthFlow } from '@/lib/api/oauth'
@@ -212,17 +213,27 @@ export default function PricingSection() {
return (
<section className="py-24 px-4 max-w-6xl mx-auto">
<div className="text-center mb-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-4xl md:text-5xl font-bold text-neutral-900 dark:text-white mb-6 tracking-tight">
Transparent Pricing
</h2>
<p className="text-xl text-neutral-600 dark:text-neutral-400">
Scale with your traffic. No hidden fees.
</p>
</div>
</motion.div>
{/* Unified Container */}
<div className="max-w-6xl mx-auto border border-neutral-200 dark:border-neutral-800 rounded-3xl bg-white/50 dark:bg-neutral-900/50 backdrop-blur-xl shadow-sm overflow-hidden mb-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="max-w-6xl mx-auto border border-neutral-200 dark:border-neutral-800 rounded-3xl bg-white/50 dark:bg-neutral-900/50 backdrop-blur-xl shadow-sm overflow-hidden mb-20"
>
{/* Top Toolbar */}
<div className="p-8 border-b border-neutral-200 dark:border-neutral-800 flex flex-col md:flex-row items-center justify-between gap-8 bg-neutral-50/50 dark:bg-neutral-900/50">
@@ -252,7 +263,7 @@ export default function PricingSection() {
<div className="bg-neutral-200 dark:bg-neutral-800 p-1 rounded-lg flex">
<button
onClick={() => setIsYearly(false)}
className={`min-w-[88px] px-4 py-2 rounded-md text-sm font-medium transition-all ${
className={`min-w-[88px] px-4 py-2 rounded-lg text-sm font-medium transition-all ${
!isYearly
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'
@@ -262,7 +273,7 @@ export default function PricingSection() {
</button>
<button
onClick={() => setIsYearly(true)}
className={`min-w-[88px] px-4 py-2 rounded-md text-sm font-medium transition-all ${
className={`min-w-[88px] px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isYearly
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'
@@ -285,9 +296,9 @@ export default function PricingSection() {
{isTeam && (
<>
<div className="absolute top-0 left-0 w-full h-1 bg-brand-orange" />
<div className="absolute top-4 right-4 bg-brand-orange/10 text-brand-orange text-[10px] font-bold px-2 py-1 rounded-full uppercase tracking-wide">
<span className="absolute top-4 right-4 badge-primary">
Most Popular
</div>
</span>
</>
)}
@@ -331,11 +342,8 @@ export default function PricingSection() {
<Button
onClick={() => handleSubscribe(plan.id)}
disabled={loadingPlan === plan.id || !!loadingPlan || !priceDetails}
className={`w-full mb-8 ${
isTeam
? 'bg-brand-orange hover:bg-brand-orange/90 text-white shadow-lg shadow-brand-orange/20'
: 'bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 hover:bg-neutral-800 dark:hover:bg-neutral-100'
}`}
variant={isTeam ? 'primary' : 'secondary'}
className="w-full mb-8"
>
{loadingPlan === plan.id ? 'Loading...' : !priceDetails ? 'Contact us' : 'Start free trial'}
</Button>
@@ -383,7 +391,7 @@ export default function PricingSection() {
</ul>
</div>
</div>
</div>
</motion.div>
</section>
)
}

View File

@@ -53,7 +53,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
{/*
<button
onClick={() => handleSwitch(null)}
className={`w-full flex items-center justify-between px-3 py-2 text-sm rounded-md transition-colors group ${
className={`w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg transition-colors group ${
!activeOrgId ? 'bg-neutral-100 dark:bg-neutral-800' : 'hover:bg-neutral-50 dark:hover:bg-neutral-800/50'
}`}
>
@@ -75,7 +75,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
<button
key={org.organization_id}
onClick={() => handleSwitch(org.organization_id)}
className={`w-full flex items-center justify-between px-3 py-2 text-sm rounded-md transition-colors mt-1 ${
className={`w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg transition-colors mt-1 ${
activeOrgId === org.organization_id ? 'bg-neutral-100 dark:bg-neutral-800' : 'hover:bg-neutral-50 dark:hover:bg-neutral-800/50'
}`}
>
@@ -97,7 +97,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
{/* Create New */}
<Link
href="/onboarding"
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-neutral-500 hover:text-blue-600 dark:text-neutral-400 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-md transition-colors mt-1"
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-neutral-500 hover:text-blue-600 dark:text-neutral-400 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-lg transition-colors mt-1"
>
<div className="h-5 w-5 rounded border border-dashed border-neutral-300 dark:border-neutral-600 flex items-center justify-center">
<PlusIcon className="h-3 w-3" />

View File

@@ -65,7 +65,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
return (
<>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full flex flex-col">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Campaigns
@@ -92,8 +92,9 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
</div>
{isLoading ? (
<div className="space-y-2 flex-1 min-h-[270px] flex flex-col items-center justify-center">
<p className="text-neutral-500">Loading...</p>
<div className="space-y-2 flex-1 min-h-[270px] flex flex-col items-center justify-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-neutral-300 dark:border-neutral-700 border-t-brand-orange rounded-full" />
<p className="text-sm text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : hasData ? (
<div className="space-y-2 flex-1 min-h-[270px]">
@@ -152,7 +153,10 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4 text-center text-neutral-500">Loading...</div>
<div className="py-8 flex flex-col items-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-neutral-300 dark:border-neutral-700 border-t-brand-orange rounded-full" />
<p className="text-sm text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : (
<>
<div className="grid grid-cols-12 gap-2 text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-2 px-2 sticky top-0 bg-white dark:bg-neutral-900 py-2 z-10">

View File

@@ -290,7 +290,7 @@ export default function Chart({
const dayTicks = interval === 'day' && chartData.length > 0 ? chartData.map((c) => c.date) : undefined
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden shadow-sm">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden shadow-sm">
{/* Stats Header (Interactive Tabs) */}
<div className="grid grid-cols-2 md:grid-cols-4 divide-x divide-neutral-200 dark:divide-neutral-800 border-b border-neutral-200 dark:border-neutral-800">
{metrics.map((item) => (

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import { formatNumber } from '@/lib/utils/format'
import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats'
import { Modal, ArrowUpRightIcon } from '@ciphera-net/ui'
import { Modal, ArrowUpRightIcon, LayoutDashboardIcon } from '@ciphera-net/ui'
interface ContentStatsProps {
topPages: TopPage[]
@@ -87,7 +87,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
return (
<>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full flex flex-col">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
@@ -107,7 +107,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors ${
activeTab === tab
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'
@@ -149,8 +149,16 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
))}
</>
) : (
<div className="h-full flex flex-col items-center justify-center">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<LayoutDashboardIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No page data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Your most visited pages will appear here as traffic arrives.
</p>
</div>
)}
</div>
@@ -163,7 +171,10 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4 text-center text-neutral-500">Loading...</div>
<div className="py-8 flex flex-col items-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-neutral-300 dark:border-neutral-700 border-t-brand-orange rounded-full" />
<p className="text-sm text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : (
(fullData.length > 0 ? fullData : data).map((page, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">

View File

@@ -4,6 +4,7 @@ import { useState } from 'react'
import { formatNumber } from '@/lib/utils/format'
import * as Flags from 'country-flag-icons/react/3x2'
import WorldMap from './WorldMap'
import { GlobeIcon } from '@ciphera-net/ui'
interface LocationProps {
countries: Array<{ country: string; pageviews: number }>
@@ -36,7 +37,19 @@ export default function Locations({ countries, cities }: LocationProps) {
const renderContent = () => {
if (activeTab === 'countries') {
if (!countries || countries.length === 0) {
return <p className="text-neutral-600 dark:text-neutral-400">No data available</p>
return (
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No location data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Visitor locations will appear here based on anonymous geographic data.
</p>
</div>
)
}
return (
<div className="space-y-4">
@@ -60,7 +73,19 @@ export default function Locations({ countries, cities }: LocationProps) {
if (activeTab === 'cities') {
if (!cities || cities.length === 0) {
return <p className="text-neutral-600 dark:text-neutral-400">No data available</p>
return (
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No city data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
City-level visitor data will appear as traffic grows.
</p>
</div>
)
}
return (
<div className="space-y-3">
@@ -81,7 +106,7 @@ export default function Locations({ countries, cities }: LocationProps) {
}
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Locations
@@ -89,7 +114,7 @@ export default function Locations({ countries, cities }: LocationProps) {
<div className="flex p-1 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
<button
onClick={() => setActiveTab('countries')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors ${
activeTab === 'countries'
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'
@@ -99,7 +124,7 @@ export default function Locations({ countries, cities }: LocationProps) {
</button>
<button
onClick={() => setActiveTab('cities')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors ${
activeTab === 'cities'
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'

View File

@@ -16,7 +16,7 @@ export default function GoalStats({ goalCounts }: GoalStatsProps) {
const hasData = list.length > 0
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full flex flex-col">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Goals & Events

View File

@@ -187,7 +187,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
return (
<>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full flex flex-col">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
@@ -207,7 +207,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors capitalize ${
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors capitalize ${
activeTab === tab
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'
@@ -226,8 +226,16 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
</div>
) : activeTab === 'map' ? (
hasData ? <WorldMap data={filterUnknown(countries)} /> : (
<div className="h-full flex flex-col items-center justify-center">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No location data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Visitor locations will appear here based on anonymous geographic data.
</p>
</div>
)
) : (
@@ -252,14 +260,22 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
))}
{Array.from({ length: emptySlots }).map((_, i) => (
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
))}
</>
) : (
<div className="h-full flex flex-col items-center justify-center">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
))}
</>
) : (
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
)
)}
<h4 className="font-semibold text-neutral-900 dark:text-white">
No location data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Visitor locations will appear here based on anonymous geographic data.
</p>
</div>
)
)}
</div>
</div>
@@ -270,7 +286,10 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4 text-center text-neutral-500">Loading...</div>
<div className="py-8 flex flex-col items-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-neutral-300 dark:border-neutral-700 border-t-brand-orange rounded-full" />
<p className="text-sm text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : (
(fullData.length > 0 ? fullData : data as any[]).map((item, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">

View File

@@ -108,7 +108,7 @@ export default function PerformanceStats({ stats, performanceByPage, siteId, sta
const summaryText = `LCP ${Math.round(stats.lcp)} ms · CLS ${Number(stats.cls.toFixed(3))} · INP ${Math.round(stats.inp)} ms`
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
{/* * One-line summary: Performance score + metric summary. Click to expand. */}
<button
type="button"

View File

@@ -13,7 +13,7 @@ export default function RealtimeVisitors({ count, siteId }: RealtimeVisitorsProp
return (
<div
onClick={() => siteId && router.push(`/sites/${siteId}/realtime`)}
className={`bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 ${siteId ? 'cursor-pointer hover:border-neutral-300 dark:hover:border-neutral-700 transition-colors' : ''}`}
className={`bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 ${siteId ? 'cursor-pointer hover:border-neutral-300 dark:hover:border-neutral-700 transition-colors' : ''}`}
>
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-neutral-600 dark:text-neutral-400">

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
import { formatNumber } from '@/lib/utils/format'
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
import { MdMonitor } from 'react-icons/md'
import { Modal } from '@ciphera-net/ui'
import { Modal, GridIcon } from '@ciphera-net/ui'
import { getBrowsers, getOS, getDevices, getScreenResolutions } from '@/lib/api/stats'
interface TechSpecsProps {
@@ -110,7 +110,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
return (
<>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full flex flex-col">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
@@ -130,7 +130,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors capitalize ${
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors capitalize ${
activeTab === tab
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'
@@ -165,8 +165,16 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
))}
</>
) : (
<div className="h-full flex flex-col items-center justify-center">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GridIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No technology data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Browser, OS, and device information will appear as visitors arrive.
</p>
</div>
)}
</div>
@@ -179,7 +187,10 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4 text-center text-neutral-500">Loading...</div>
<div className="py-8 flex flex-col items-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-neutral-300 dark:border-neutral-700 border-t-brand-orange rounded-full" />
<p className="text-sm text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : (
(fullData.length > 0 ? fullData : data).map((item, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">

View File

@@ -1,6 +1,7 @@
'use client'
import { formatNumber } from '@/lib/utils/format'
import { LayoutDashboardIcon } from '@ciphera-net/ui'
interface TopPagesProps {
pages: Array<{ path: string; pageviews: number }>
@@ -9,17 +10,27 @@ interface TopPagesProps {
export default function TopPages({ pages }: TopPagesProps) {
if (!pages || pages.length === 0) {
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 flex flex-col">
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
Top Pages
</h3>
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
<div className="flex-1 flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<LayoutDashboardIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No page data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Your most visited pages will appear here as traffic arrives.
</p>
</div>
</div>
)
}
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
Top Pages
</h3>

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import { formatNumber } from '@/lib/utils/format'
import { getReferrerIcon } from '@/lib/utils/icons'
import { Modal } from '@ciphera-net/ui'
import { Modal, GlobeIcon } from '@ciphera-net/ui'
import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
interface TopReferrersProps {
@@ -55,7 +55,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
return (
<>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full flex flex-col">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Top Referrers
@@ -93,8 +93,16 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
))}
</>
) : (
<div className="h-full flex flex-col items-center justify-center">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No referrers yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
Traffic sources will appear here when visitors come from external sites.
</p>
</div>
)}
</div>
@@ -107,7 +115,10 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4 text-center text-neutral-500">Loading...</div>
<div className="py-8 flex flex-col items-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-neutral-300 dark:border-neutral-700 border-t-brand-orange rounded-full" />
<p className="text-sm text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : (
(fullData.length > 0 ? fullData : filteredReferrers).map((ref, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">

View File

@@ -501,7 +501,7 @@ export default function OrganizationSettings() {
<p className="text-sm text-neutral-500 dark:text-neutral-400">Irreversible actions for this organization.</p>
</div>
<div className="p-4 border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/10 rounded-xl flex items-center justify-between">
<div className="p-4 border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/10 rounded-2xl flex items-center justify-between">
<div>
<h3 className="font-medium text-red-900 dark:text-red-200">Delete Organization</h3>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">Permanently delete this organization and all its data.</p>
@@ -524,7 +524,7 @@ export default function OrganizationSettings() {
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Organization Members</h2>
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">Manage who has access to this organization.</p>
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4">
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
<h3 className="text-sm font-medium text-neutral-900 dark:text-white mb-3">Invite New Member</h3>
<form onSubmit={handleSendInvite} className="flex gap-3 items-end">
<div className="flex-1">
@@ -569,7 +569,7 @@ export default function OrganizationSettings() {
{/* Members List */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider">Active Members</h3>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
{isLoadingMembers ? (
<div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />
@@ -613,7 +613,7 @@ export default function OrganizationSettings() {
{invitations.length > 0 && (
<div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider">Pending Invitations</h3>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
{invitations.map((invite) => (
<div key={invite.id} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -656,7 +656,7 @@ export default function OrganizationSettings() {
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />
</div>
) : !subscription ? (
<div className="p-8 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-200 dark:border-neutral-800">
<div className="p-8 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-200 dark:border-neutral-800">
<p className="text-neutral-500">Could not load subscription details.</p>
<Button
variant="ghost"
@@ -669,7 +669,7 @@ export default function OrganizationSettings() {
) : (
<div className="space-y-8">
{/* Current Plan */}
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6">
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-1">Current Plan</h3>
@@ -747,7 +747,7 @@ export default function OrganizationSettings() {
</div>
{!subscription.has_payment_method && (
<div className="p-6 bg-brand-orange/5 border border-brand-orange/20 rounded-xl">
<div className="p-6 bg-brand-orange/5 border border-brand-orange/20 rounded-2xl">
<h3 className="font-medium text-brand-orange mb-2">Upgrade to Pro</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
Get higher limits, more data retention, and priority support.
@@ -761,7 +761,7 @@ export default function OrganizationSettings() {
{/* Invoice History */}
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-4">Invoice History</h3>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden">
{isLoadingInvoices ? (
<div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />
@@ -837,7 +837,7 @@ export default function OrganizationSettings() {
</div>
{/* Advanced Filters */}
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4 mb-6">
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-1">
<label className="block text-xs font-medium text-neutral-500 uppercase">Log ID</label>
@@ -897,7 +897,7 @@ export default function OrganizationSettings() {
</div>
{/* Table */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden">
{isLoadingAudit ? (
<div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />

View File

@@ -2,7 +2,7 @@
import Link from 'next/link'
import { Site } from '@/lib/api/sites'
import { BarChartIcon, SettingsIcon, BookOpenIcon, ExternalLinkIcon } from '@ciphera-net/ui'
import { BarChartIcon, SettingsIcon, BookOpenIcon, ExternalLinkIcon, Button } from '@ciphera-net/ui'
import { useAuth } from '@/lib/auth/context'
interface SiteListProps {
@@ -18,7 +18,7 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-48 animate-pulse rounded-xl bg-neutral-100 dark:bg-neutral-800" />
<div key={i} className="h-48 animate-pulse rounded-2xl bg-neutral-100 dark:bg-neutral-800" />
))}
</div>
)
@@ -38,7 +38,7 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
{sites.map((site) => (
<div
key={site.id}
className="group relative flex flex-col rounded-xl border border-neutral-200 bg-white p-6 shadow-sm transition-all hover:shadow-md dark:border-neutral-800 dark:bg-neutral-900"
className="group relative flex flex-col rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm transition-all hover:shadow-md dark:border-neutral-800 dark:bg-neutral-900"
>
{/* Header: Icon + Name + Live Status */}
<div className="flex items-start justify-between mb-6">
@@ -94,10 +94,12 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
<div className="mt-auto flex gap-2">
<Link
href={`/sites/${site.id}`}
className="btn-primary flex-1 justify-center text-center text-sm inline-flex items-center gap-2"
className="flex-1"
>
<BarChartIcon className="w-4 h-4" />
View Dashboard
<Button variant="primary" className="w-full justify-center text-sm">
<BarChartIcon className="w-4 h-4" />
View Dashboard
</Button>
</Link>
{(user?.role === 'owner' || user?.role === 'admin') && (
<button
@@ -114,7 +116,7 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
))}
{/* Resources Card */}
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-neutral-300 bg-neutral-50 p-6 text-center dark:border-neutral-700 dark:bg-neutral-900/50">
<div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-neutral-300 bg-neutral-50 p-6 text-center dark:border-neutral-700 dark:bg-neutral-900/50">
<div className="mb-3 rounded-full bg-neutral-200 p-3 dark:bg-neutral-800">
<BookOpenIcon className="h-6 w-6 text-neutral-500" />
</div>

View File

@@ -196,7 +196,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
</div>
{generatedUrl && (
<div className="mt-6 p-4 bg-neutral-50 dark:bg-neutral-900 rounded-xl border border-neutral-200 dark:border-neutral-800 flex items-center justify-between group">
<div className="mt-6 p-4 bg-neutral-50 dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 flex items-center justify-between group">
<code className="text-sm break-all text-brand-orange font-mono">{generatedUrl}</code>
<Button
variant="secondary"