[PULSE-43] Design system standardization and branding alignment #11

Merged
uz1mani merged 16 commits from staging into main 2026-02-05 17:18:07 +00:00
35 changed files with 635 additions and 219 deletions

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import { motion } from 'framer-motion'
import { CheckCircleIcon, XIcon } from '@ciphera-net/ui' import { CheckCircleIcon, XIcon } from '@ciphera-net/ui'
function ComparisonTable({ title, competitors }: { title: string, competitors: { name: string, isPulse: boolean, features: Record<string, boolean | string> }[] }) { function ComparisonTable({ title, competitors }: { title: string, competitors: { name: string, isPulse: boolean, features: Record<string, boolean | string> }[] }) {
@@ -68,7 +69,12 @@ export default function AboutPage() {
</div> </div>
<div className="flex-grow w-full max-w-4xl mx-auto px-4 pt-20 pb-10 z-10"> <div className="flex-grow w-full max-w-4xl mx-auto px-4 pt-20 pb-10 z-10">
<div className="text-center mb-16"> <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6"> <h1 className="text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6">
Why Pulse? Why Pulse?
</h1> </h1>
@@ -76,14 +82,19 @@ export default function AboutPage() {
We built Pulse because we were tired of complex, invasive analytics tools. We built Pulse because we were tired of complex, invasive analytics tools.
Here is how we stack up against the giants. Here is how we stack up against the giants.
</p> </p>
</div> </motion.div>
<div className="prose prose-neutral dark:prose-invert max-w-none mb-16"> <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="prose prose-neutral dark:prose-invert max-w-none mb-16"
>
<p className="text-lg text-neutral-600 dark:text-neutral-400"> <p className="text-lg text-neutral-600 dark:text-neutral-400">
Most analytics tools are overkill. They track everything, slow down your site, and require annoying cookie banners. Most analytics tools are overkill. They track everything, slow down your site, and require annoying cookie banners.
Pulse is different. We focus on the metrics that actually mattervisitors, pageviews, and sourceswhile respecting user privacy. Pulse is different. We focus on the metrics that actually mattervisitors, pageviews, and sourceswhile respecting user privacy.
</p> </p>
</div> </motion.div>
{/* * Comparison: Pulse vs Google Analytics */} {/* * Comparison: Pulse vs Google Analytics */}
<ComparisonTable <ComparisonTable
@@ -147,14 +158,20 @@ export default function AboutPage() {
]} ]}
/> />
<div className="mt-8 p-6 bg-neutral-100 dark:bg-neutral-800/50 rounded-xl border border-neutral-200 dark:border-neutral-800"> <motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mt-8 p-6 bg-neutral-100 dark:bg-neutral-800/50 rounded-xl border border-neutral-200 dark:border-neutral-800"
>
<h3 className="text-lg font-bold mb-2 text-neutral-900 dark:text-white">What about Plausible?</h3> <h3 className="text-lg font-bold mb-2 text-neutral-900 dark:text-white">What about Plausible?</h3>
<p className="text-neutral-600 dark:text-neutral-400 text-sm"> <p className="text-neutral-600 dark:text-neutral-400 text-sm">
We love Plausible! They paved the way for privacy-friendly analytics. We love Plausible! They paved the way for privacy-friendly analytics.
Pulse offers a similar philosophy but with a focus on even deeper integration with the Ciphera ecosystem Pulse offers a similar philosophy but with a focus on even deeper integration with the Ciphera ecosystem
and more flexible pricing for developers. and more flexible pricing for developers.
</p> </p>
</div> </motion.div>
</div> </div>
</div> </div>

View File

@@ -131,7 +131,7 @@ export default function FAQPage() {
</p> </p>
<a <a
href="mailto:support@ciphera.net" href="mailto:support@ciphera.net"
className="btn-secondary inline-flex" className="inline-flex items-center justify-center gap-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white px-5 py-2.5 rounded-xl font-medium hover:bg-neutral-50 dark:hover:bg-neutral-800 shadow-sm hover:shadow-md dark:shadow-none transition-all duration-200"
> >
Contact us Contact us
</a> </a>

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { motion } from 'framer-motion'
import { ArrowRightIcon } from '@ciphera-net/ui' import { ArrowRightIcon } from '@ciphera-net/ui'
const integrations = [ const integrations = [
@@ -62,22 +63,33 @@ export default function IntegrationsPage() {
</div> </div>
<div className="flex-grow w-full max-w-6xl mx-auto px-4 pt-20 pb-10 z-10"> <div className="flex-grow w-full max-w-6xl mx-auto px-4 pt-20 pb-10 z-10">
<div className="text-center mb-16"> <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6"> <h1 className="text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6">
Integrations Integrations
</h1> </h1>
<p className="text-xl text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto leading-relaxed"> <p className="text-xl text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto leading-relaxed">
Connect Pulse with your favorite frameworks and platforms in minutes. Connect Pulse with your favorite frameworks and platforms in minutes.
</p> </p>
</div> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{integrations.map((integration) => ( {integrations.map((integration, i) => (
<Link <motion.div
key={integration.id} key={integration.id}
href={`/integrations/${integration.id}`} initial={{ opacity: 0, y: 20 }}
className="group relative p-8 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm border border-neutral-200 dark:border-neutral-800 rounded-2xl hover:border-brand-orange/50 dark:hover:border-brand-orange/50 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl" whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1 }}
> >
<Link
href={`/integrations/${integration.id}`}
className="group relative p-8 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm border border-neutral-200 dark:border-neutral-800 rounded-2xl hover:border-brand-orange/50 dark:hover:border-brand-orange/50 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl block"
>
<div className="flex items-start justify-between mb-6"> <div className="flex items-start justify-between mb-6">
<div className="p-3 bg-neutral-100 dark:bg-neutral-800 rounded-xl group-hover:scale-110 transition-transform duration-300"> <div className="p-3 bg-neutral-100 dark:bg-neutral-800 rounded-xl group-hover:scale-110 transition-transform duration-300">
{integration.icon} {integration.icon}
@@ -95,10 +107,17 @@ export default function IntegrationsPage() {
View Guide <span aria-hidden="true">&rarr;</span> View Guide <span aria-hidden="true">&rarr;</span>
</span> </span>
</Link> </Link>
</motion.div>
))} ))}
{/* * Request Integration Card */} {/* * Request Integration Card */}
<div className="p-8 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-2xl flex flex-col items-center justify-center text-center"> <motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: integrations.length * 0.1 }}
className="p-8 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-2xl flex flex-col items-center justify-center text-center"
>
<h3 className="text-lg font-bold text-neutral-900 dark:text-white mb-2"> <h3 className="text-lg font-bold text-neutral-900 dark:text-white mb-2">
Missing something? Missing something?
</h3> </h3>
@@ -111,7 +130,7 @@ export default function IntegrationsPage() {
> >
Request Integration Request Integration
</a> </a>
</div> </motion.div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,8 @@
'use client' 'use client'
import { OfflineBanner } from '@/components/OfflineBanner' import { OfflineBanner } from '@/components/OfflineBanner'
import { Header, Footer, GridIcon } from '@ciphera-net/ui' import { Footer } from '@/components/Footer'
import { Header, GridIcon } from '@ciphera-net/ui'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus' import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus'
import Link from 'next/link' import Link from 'next/link'
@@ -82,8 +83,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode
<Footer <Footer
LinkComponent={Link} LinkComponent={Link}
appName="Pulse" appName="Pulse"
showPricing={true} isAuthenticated={!!auth.user}
showSecurity={false}
/> />
</> </>
) )

View File

@@ -28,12 +28,12 @@ export default function NotFound() {
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link href="/"> <Link href="/">
<Button className="btn-primary px-8 py-3 shadow-lg shadow-brand-orange/20"> <Button variant="primary" className="px-8 py-3 shadow-lg shadow-brand-orange/20">
Go back home Go back home
</Button> </Button>
</Link> </Link>
<Link href="/faq"> <Link href="/faq">
<Button variant="secondary" className="btn-secondary px-8 py-3 backdrop-blur-sm"> <Button variant="secondary" className="px-8 py-3">
View FAQ View FAQ
</Button> </Button>
</Link> </Link>

View File

@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { motion } from 'framer-motion'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { initiateOAuthFlow, initiateSignupFlow } from '@/lib/api/oauth' import { initiateOAuthFlow, initiateSignupFlow } from '@/lib/api/oauth'
import { listSites, deleteSite, type Site } from '@/lib/api/sites' import { listSites, deleteSite, type Site } from '@/lib/api/sites'
@@ -172,16 +173,26 @@ export default function HomePage() {
<div className="flex-grow w-full max-w-6xl mx-auto px-4 pt-20 pb-10 z-10"> <div className="flex-grow w-full max-w-6xl mx-auto px-4 pt-20 pb-10 z-10">
{/* * --- 2. BADGE --- */} {/* * --- 2. BADGE --- */}
<div className="inline-flex justify-center mb-8 animate-fade-in w-full"> <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="inline-flex justify-center mb-8 w-full"
>
<span className="badge-primary"> <span className="badge-primary">
<span className="w-1.5 h-1.5 rounded-full bg-brand-orange animate-pulse" /> <span className="w-1.5 h-1.5 rounded-full bg-brand-orange animate-pulse" />
Privacy-First Analytics Privacy-First Analytics
</span> </span>
</div> </motion.div>
{/* * --- 3. HEADLINE --- */} {/* * --- 3. HEADLINE --- */}
<div className="text-center mb-20"> <div className="text-center mb-20">
<h1 className="text-5xl md:text-7xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6"> <motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="text-5xl md:text-7xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6"
>
Simple analytics for <br /> Simple analytics for <br />
<span className="relative inline-block"> <span className="relative inline-block">
<span className="gradient-text">privacy-conscious</span> <span className="gradient-text">privacy-conscious</span>
@@ -191,22 +202,32 @@ export default function HomePage() {
</svg> </svg>
</span> </span>
{' '}apps. {' '}apps.
</h1> </motion.h1>
<p className="text-xl text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto mb-10 leading-relaxed"> <motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-xl text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto mb-10 leading-relaxed"
>
Respect your users' privacy while getting the insights you need. Respect your users' privacy while getting the insights you need.
No cookies, no IP tracking, fully GDPR compliant. No cookies, no IP tracking, fully GDPR compliant.
</p> </motion.p>
{/* * --- 4. CTAs --- */} {/* * --- 4. CTAs --- */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-20"> <motion.div
<Button onClick={() => initiateOAuthFlow()} className="btn-primary px-8 py-4 text-lg shadow-lg shadow-brand-orange/20"> initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-20"
>
<Button onClick={() => initiateOAuthFlow()} variant="primary" className="px-8 py-4 text-lg shadow-lg shadow-brand-orange/20">
Get Started Get Started
</Button> </Button>
<Button variant="secondary" onClick={() => initiateSignupFlow()} className="btn-secondary px-8 py-4 text-lg backdrop-blur-sm"> <Button onClick={() => initiateSignupFlow()} variant="secondary" className="px-8 py-4 text-lg">
Create Account Create Account
</Button> </Button>
</div> </motion.div>
</div> </div>
{/* * NEW: DASHBOARD PREVIEW */} {/* * NEW: DASHBOARD PREVIEW */}
@@ -219,7 +240,14 @@ export default function HomePage() {
{ icon: BarChartIcon, title: "Simple Insights", desc: "Get the metrics that matter without the clutter. Page views, visitors, and sources." }, { icon: BarChartIcon, title: "Simple Insights", desc: "Get the metrics that matter without the clutter. Page views, visitors, and sources." },
{ icon: ZapIcon, title: "Lightweight", desc: "Our script is less than 1kb. It won't slow down your site or affect your SEO." } { icon: ZapIcon, title: "Lightweight", desc: "Our script is less than 1kb. It won't slow down your site or affect your SEO." }
].map((feature, i) => ( ].map((feature, i) => (
<div key={i} className="card-glass p-8 hover:-translate-y-1 hover:shadow-xl transition-all duration-300 group"> <motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1 }}
className="card-glass p-8 hover:-translate-y-1 hover:shadow-xl transition-all duration-300 group"
>
<div className="w-12 h-12 rounded-xl bg-brand-orange/10 flex items-center justify-center mb-6 text-brand-orange group-hover:scale-110 transition-transform duration-300"> <div className="w-12 h-12 rounded-xl bg-brand-orange/10 flex items-center justify-center mb-6 text-brand-orange group-hover:scale-110 transition-transform duration-300">
<feature.icon className="w-6 h-6" /> <feature.icon className="w-6 h-6" />
</div> </div>
@@ -227,7 +255,7 @@ export default function HomePage() {
<p className="text-neutral-600 dark:text-neutral-400 leading-relaxed"> <p className="text-neutral-600 dark:text-neutral-400 leading-relaxed">
{feature.desc} {feature.desc}
</p> </p>
</div> </motion.div>
))} ))}
</div> </div>
@@ -235,13 +263,19 @@ export default function HomePage() {
<ComparisonSection /> <ComparisonSection />
{/* * NEW: CTA BOTTOM */} {/* * NEW: CTA BOTTOM */}
<div className="text-center mb-20"> <motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-20"
>
<h2 className="text-3xl font-bold text-neutral-900 dark:text-white mb-6">Ready to switch?</h2> <h2 className="text-3xl font-bold text-neutral-900 dark:text-white mb-6">Ready to switch?</h2>
<Button onClick={() => initiateOAuthFlow()} className="btn-primary px-8 py-4 text-lg shadow-lg shadow-brand-orange/20"> <Button onClick={() => initiateOAuthFlow()} variant="primary" className="px-8 py-4 text-lg shadow-lg shadow-brand-orange/20">
Start your free trial Start your free trial
</Button> </Button>
<p className="mt-4 text-sm text-neutral-500">No credit card required • Cancel anytime</p> <p className="mt-4 text-sm text-neutral-500">No credit card required • Cancel anytime</p>
</div> </motion.div>
</div> </div>
</div> </div>
@@ -265,26 +299,32 @@ export default function HomePage() {
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-3 py-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700"> <span className="text-sm font-medium text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-3 py-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700">
Limit reached (1/1) Limit reached (1/1)
</span> </span>
<Link href="/pricing" className="btn-primary text-sm bg-brand-orange hover:bg-brand-orange/90 border-transparent text-white shadow-lg shadow-brand-orange/20"> <Link href="/pricing">
Upgrade <Button variant="primary" className="text-sm">
Upgrade
</Button>
</Link> </Link>
</div> </div>
) : ( ) : (
<Link href="/sites/new" className="btn-primary text-sm">Add New Site</Link> <Link href="/sites/new">
<Button variant="primary" className="text-sm">
Add New Site
</Button>
</Link>
)} )}
</div> </div>
{/* * Global Overview */} {/* * Global Overview */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="rounded-xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900"> <div className="rounded-2xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
<p className="text-sm text-neutral-500 dark:text-neutral-400">Total Sites</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Total Sites</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-white">{sites.length}</p> <p className="text-2xl font-bold text-neutral-900 dark:text-white">{sites.length}</p>
</div> </div>
<div className="rounded-xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900"> <div className="rounded-2xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
<p className="text-sm text-neutral-500 dark:text-neutral-400">Total Visitors (24h)</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Total Visitors (24h)</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-white">--</p> <p className="text-2xl font-bold text-neutral-900 dark:text-white">--</p>
</div> </div>
<div className="rounded-xl border border-neutral-200 bg-brand-orange/10 p-4 dark:border-neutral-800"> <div className="rounded-2xl border border-neutral-200 bg-brand-orange/10 p-4 dark:border-neutral-800">
<p className="text-sm text-brand-orange">Plan & usage</p> <p className="text-sm text-brand-orange">Plan & usage</p>
{subscriptionLoading ? ( {subscriptionLoading ? (
<p className="text-lg font-bold text-brand-orange">...</p> <p className="text-lg font-bold text-brand-orange">...</p>

View File

@@ -5,7 +5,7 @@ import { useParams, useSearchParams, useRouter } from 'next/navigation'
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats' import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
import { toast } from '@ciphera-net/ui' import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import { LoadingOverlay } from '@ciphera-net/ui' import { LoadingOverlay, Button } from '@ciphera-net/ui'
import Chart from '@/components/dashboard/Chart' import Chart from '@/components/dashboard/Chart'
import TopPages from '@/components/dashboard/ContentStats' import TopPages from '@/components/dashboard/ContentStats'
import TopReferrers from '@/components/dashboard/TopReferrers' import TopReferrers from '@/components/dashboard/TopReferrers'
@@ -191,7 +191,7 @@ export default function PublicDashboardPage() {
if (isPasswordProtected && !data) { if (isPasswordProtected && !data) {
return ( return (
<div className="min-h-screen flex items-center justify-center px-4"> <div className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md w-full bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-8 shadow-lg"> <div className="max-w-md w-full bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-8 shadow-lg">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="w-12 h-12 bg-brand-orange/10 rounded-xl flex items-center justify-center mx-auto mb-4 text-brand-orange"> <div className="w-12 h-12 bg-brand-orange/10 rounded-xl flex items-center justify-center mx-auto mb-4 text-brand-orange">
<ZapIcon className="w-6 h-6" /> <ZapIcon className="w-6 h-6" />
@@ -225,12 +225,13 @@ export default function PublicDashboardPage() {
apiUrl={process.env.NEXT_PUBLIC_CAPTCHA_API_URL} apiUrl={process.env.NEXT_PUBLIC_CAPTCHA_API_URL}
/> />
</div> </div>
<button <Button
type="submit" type="submit"
className="w-full btn-primary" variant="primary"
className="w-full"
> >
Access Dashboard Access Dashboard
</button> </Button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { ApiError } from '@/lib/api/client' import { ApiError } from '@/lib/api/client'
import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats } from '@/lib/api/funnels' import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats } from '@/lib/api/funnels'
import { toast, LoadingOverlay, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, useTheme } from '@ciphera-net/ui' import { toast, LoadingOverlay, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, useTheme, Button } from '@ciphera-net/ui'
import Link from 'next/link' import Link from 'next/link'
import { import {
BarChart, BarChart,
@@ -107,8 +107,10 @@ export default function FunnelReportPage() {
return ( return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"> <div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<p className="text-neutral-600 dark:text-neutral-400">Access denied</p> <p className="text-neutral-600 dark:text-neutral-400">Access denied</p>
<Link href={`/sites/${siteId}/funnels`} className="btn-primary mt-4 inline-block"> <Link href={`/sites/${siteId}/funnels`}>
Back to Funnels <Button variant="primary" className="mt-4">
Back to Funnels
</Button>
</Link> </Link>
</div> </div>
) )
@@ -118,9 +120,9 @@ export default function FunnelReportPage() {
return ( return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"> <div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<p className="text-neutral-600 dark:text-neutral-400 mb-4">Unable to load funnel</p> <p className="text-neutral-600 dark:text-neutral-400 mb-4">Unable to load funnel</p>
<button type="button" onClick={() => loadData()} className="btn-primary"> <Button type="button" onClick={() => loadData()} variant="primary">
Try again Try again
</button> </Button>
</div> </div>
) )
} }
@@ -195,7 +197,7 @@ export default function FunnelReportPage() {
</div> </div>
{/* Chart */} {/* Chart */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden shadow-sm p-6 mb-8"> <div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden shadow-sm p-6 mb-8">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-6"> <h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-6">
Funnel Visualization Funnel Visualization
</h3> </h3>
@@ -260,7 +262,7 @@ export default function FunnelReportPage() {
</div> </div>
{/* Detailed Stats Table */} {/* Detailed Stats 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">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left text-sm"> <table className="w-full text-left text-sm">
<thead className="bg-neutral-50 dark:bg-neutral-800/50 border-b border-neutral-200 dark:border-neutral-800"> <thead className="bg-neutral-50 dark:bg-neutral-800/50 border-b border-neutral-200 dark:border-neutral-800">

View File

@@ -110,7 +110,7 @@ export default function CreateFunnelPage() {
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 mb-6"> <div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 mb-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1"> <label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
@@ -144,7 +144,7 @@ export default function CreateFunnelPage() {
</div> </div>
{steps.map((step, index) => ( {steps.map((step, index) => (
<div key={index} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4"> <div key={index} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="mt-3 text-neutral-400"> <div className="mt-3 text-neutral-400">
<div className="w-6 h-6 rounded-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-sm font-medium text-neutral-600 dark:text-neutral-400"> <div className="w-6 h-6 rounded-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-sm font-medium text-neutral-600 dark:text-neutral-400">
@@ -171,7 +171,7 @@ export default function CreateFunnelPage() {
<select <select
value={step.type} value={step.type}
onChange={(e) => handleUpdateStep(index, 'type', e.target.value)} onChange={(e) => handleUpdateStep(index, 'type', e.target.value)}
className="w-24 px-2 py-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm focus:ring-2 focus:ring-brand-orange/20 focus:border-brand-orange outline-none" className="w-24 px-2 py-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-lg text-sm focus:ring-2 focus:ring-brand-orange/20 focus:border-brand-orange outline-none"
> >
<option value="exact">Exact</option> <option value="exact">Exact</option>
<option value="contains">Contains</option> <option value="contains">Contains</option>
@@ -215,16 +215,15 @@ export default function CreateFunnelPage() {
</div> </div>
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Link <Link href={`/sites/${siteId}/funnels`}>
href={`/sites/${siteId}/funnels`} <Button variant="secondary">
className="btn-secondary" Cancel
> </Button>
Cancel
</Link> </Link>
<Button <Button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="btn-primary" variant="primary"
> >
{saving ? 'Creating...' : 'Create Funnel'} {saving ? 'Creating...' : 'Create Funnel'}
</Button> </Button>

View File

@@ -3,7 +3,7 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels' import { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels'
import { toast, LoadingOverlay, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon } from '@ciphera-net/ui' import { toast, LoadingOverlay, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui'
import Link from 'next/link' import Link from 'next/link'
export default function FunnelsPage() { export default function FunnelsPage() {
@@ -66,18 +66,17 @@ export default function FunnelsPage() {
</p> </p>
</div> </div>
<div className="ml-auto"> <div className="ml-auto">
<Link <Link href={`/sites/${siteId}/funnels/new`}>
href={`/sites/${siteId}/funnels/new`} <Button variant="primary" className="inline-flex items-center gap-2">
className="btn-primary inline-flex items-center gap-2" <PlusIcon className="w-4 h-4" />
> <span>Create Funnel</span>
<PlusIcon className="w-4 h-4" /> </Button>
<span>Create Funnel</span>
</Link> </Link>
</div> </div>
</div> </div>
{funnels.length === 0 ? ( {funnels.length === 0 ? (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-12 text-center"> <div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-12 text-center">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 mx-auto mb-4 w-fit"> <div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 mx-auto mb-4 w-fit">
<ArrowRightIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <ArrowRightIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div> </div>
@@ -87,12 +86,11 @@ export default function FunnelsPage() {
<p className="text-neutral-600 dark:text-neutral-400 mb-6 max-w-md mx-auto"> <p className="text-neutral-600 dark:text-neutral-400 mb-6 max-w-md mx-auto">
Create a funnel to track how users move through your site and where they drop off. Create a funnel to track how users move through your site and where they drop off.
</p> </p>
<Link <Link href={`/sites/${siteId}/funnels/new`}>
href={`/sites/${siteId}/funnels/new`} <Button variant="primary" className="inline-flex items-center gap-2">
className="btn-primary inline-flex items-center gap-2" <PlusIcon className="w-4 h-4" />
> <span>Create Funnel</span>
<PlusIcon className="w-4 h-4" /> </Button>
<span>Create Funnel</span>
</Link> </Link>
</div> </div>
) : ( ) : (
@@ -103,7 +101,7 @@ export default function FunnelsPage() {
href={`/sites/${siteId}/funnels/${funnel.id}`} href={`/sites/${siteId}/funnels/${funnel.id}`}
className="block group" className="block group"
> >
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 hover:border-brand-orange/50 transition-colors"> <div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 hover:border-brand-orange/50 transition-colors">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-medium text-neutral-900 dark:text-white group-hover:text-brand-orange transition-colors"> <h3 className="text-lg font-medium text-neutral-900 dark:text-white group-hover:text-brand-orange transition-colors">

View File

@@ -3,12 +3,13 @@
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { motion } from 'framer-motion'
import { getSite, type Site } from '@/lib/api/sites' import { getSite, type Site } from '@/lib/api/sites'
import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard, getPerformanceByPage, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats' import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard, getPerformanceByPage, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format' import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
import { toast } from '@ciphera-net/ui' import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import { LoadingOverlay } from '@ciphera-net/ui' import { LoadingOverlay, Button } from '@ciphera-net/ui'
import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui' import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui'
import ExportModal from '@/components/dashboard/ExportModal' import ExportModal from '@/components/dashboard/ExportModal'
import ContentStats from '@/components/dashboard/ContentStats' import ContentStats from '@/components/dashboard/ContentStats'
@@ -225,7 +226,12 @@ export default function SiteDashboardPage() {
} }
return ( return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"
>
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -239,7 +245,10 @@ export default function SiteDashboardPage() {
</div> </div>
{/* Realtime Indicator */} {/* Realtime Indicator */}
<div className="flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20"> <button
onClick={() => router.push(`/sites/${siteId}/realtime`)}
className="flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20 hover:bg-green-500/20 transition-colors cursor-pointer"
>
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span> <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
@@ -247,7 +256,7 @@ export default function SiteDashboardPage() {
<span className="text-sm font-medium text-green-700 dark:text-green-400"> <span className="text-sm font-medium text-green-700 dark:text-green-400">
{realtime} current visitors {realtime} current visitors
</span> </span>
</div> </button>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -297,19 +306,21 @@ export default function SiteDashboardPage() {
{ value: 'custom', label: 'Custom' }, { value: 'custom', label: 'Custom' },
]} ]}
/> />
<button <Button
onClick={() => router.push(`/sites/${siteId}/funnels`)} onClick={() => router.push(`/sites/${siteId}/funnels`)}
className="btn-secondary text-sm" variant="secondary"
className="text-sm"
> >
Funnels Funnels
</button> </Button>
{canEdit && ( {canEdit && (
<button <Button
onClick={() => router.push(`/sites/${siteId}/settings`)} onClick={() => router.push(`/sites/${siteId}/settings`)}
className="btn-secondary text-sm" variant="secondary"
className="text-sm"
> >
Settings Settings
</button> </Button>
)} )}
</div> </div>
</div> </div>
@@ -412,6 +423,6 @@ export default function SiteDashboardPage() {
topPages={topPages} topPages={topPages}
topReferrers={topReferrers} topReferrers={topReferrers}
/> />
</div> </motion.div>
) )
} }

View File

@@ -6,7 +6,8 @@ import { getSite, type Site } from '@/lib/api/sites'
import { getRealtimeVisitors, getSessionDetails, type Visitor, type SessionEvent } from '@/lib/api/realtime' import { getRealtimeVisitors, getSessionDetails, type Visitor, type SessionEvent } from '@/lib/api/realtime'
import { toast } from '@ciphera-net/ui' import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import { LoadingOverlay } from '@ciphera-net/ui' import { LoadingOverlay, UserIcon } from '@ciphera-net/ui'
import { motion, AnimatePresence } from 'framer-motion'
function formatTimeAgo(dateString: string) { function formatTimeAgo(dateString: string) {
const date = new Date(dateString) const date = new Date(dateString)
@@ -93,7 +94,7 @@ export default function RealtimePage() {
if (!site) return <div className="p-8">Site not found</div> if (!site) return <div className="p-8">Site not found</div>
return ( return (
<div className="w-full max-w-7xl mx-auto px-4 sm:px-6 py-8 h-[calc(100vh-64px)] flex flex-col"> <div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8 h-[calc(100vh-64px)] flex flex-col">
<div className="mb-6 flex items-center justify-between"> <div className="mb-6 flex items-center justify-between">
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
@@ -116,25 +117,38 @@ export default function RealtimePage() {
<div className="flex flex-1 gap-6 min-h-0"> <div className="flex flex-1 gap-6 min-h-0">
{/* Visitors List */} {/* Visitors List */}
<div className="w-1/3 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden flex flex-col bg-white dark:bg-neutral-900"> <div className="w-1/3 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden flex flex-col bg-white dark:bg-neutral-900">
<div className="p-4 border-b border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800/50"> <div className="p-4 border-b border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800/50">
<h2 className="font-semibold text-neutral-900 dark:text-white">Active Sessions</h2> <h2 className="font-semibold text-neutral-900 dark:text-white">Active Sessions</h2>
</div> </div>
<div className="overflow-y-auto flex-1"> <div className="overflow-y-auto flex-1">
{visitors.length === 0 ? ( {visitors.length === 0 ? (
<div className="p-8 text-center text-neutral-500"> <div className="p-8 flex flex-col items-center justify-center text-center gap-3">
No active visitors right now. <div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-3">
<UserIcon className="w-6 h-6 text-neutral-500 dark:text-neutral-400" />
</div>
<p className="text-sm font-medium text-neutral-900 dark:text-white">
No active visitors right now
</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
New visitors will appear here in real-time
</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-neutral-100 dark:divide-neutral-800"> <div className="divide-y divide-neutral-100 dark:divide-neutral-800">
{visitors.map((visitor) => ( <AnimatePresence mode="popLayout">
<button {visitors.map((visitor) => (
key={visitor.session_id} <motion.button
onClick={() => handleSelectVisitor(visitor)} key={visitor.session_id}
className={`w-full text-left p-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors ${ initial={{ opacity: 0, x: -10 }}
selectedVisitor?.session_id === visitor.session_id ? 'bg-neutral-50 dark:bg-neutral-800/50 ring-1 ring-inset ring-neutral-200 dark:ring-neutral-700' : '' animate={{ opacity: 1, x: 0 }}
}`} exit={{ opacity: 0, x: -10 }}
> transition={{ duration: 0.2 }}
onClick={() => handleSelectVisitor(visitor)}
className={`w-full text-left p-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors ${
selectedVisitor?.session_id === visitor.session_id ? 'bg-neutral-50 dark:bg-neutral-800/50 ring-1 ring-inset ring-neutral-200 dark:ring-neutral-700' : ''
}`}
>
<div className="flex justify-between items-start mb-1"> <div className="flex justify-between items-start mb-1">
<div className="font-medium text-neutral-900 dark:text-white truncate pr-2"> <div className="font-medium text-neutral-900 dark:text-white truncate pr-2">
{visitor.country ? `${getFlagEmoji(visitor.country)} ${visitor.city || 'Unknown City'}` : 'Unknown Location'} {visitor.country ? `${getFlagEmoji(visitor.country)} ${visitor.city || 'Unknown City'}` : 'Unknown Location'}
@@ -156,15 +170,16 @@ export default function RealtimePage() {
{visitor.pageviews} views {visitor.pageviews} views
</span> </span>
</div> </div>
</button> </motion.button>
))} ))}
</AnimatePresence>
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* Session Details */} {/* Session Details */}
<div className="flex-1 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden flex flex-col bg-white dark:bg-neutral-900"> <div className="flex-1 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden flex flex-col bg-white dark:bg-neutral-900">
<div className="p-4 border-b border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800/50 flex justify-between items-center"> <div className="p-4 border-b border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800/50 flex justify-between items-center">
<h2 className="font-semibold text-neutral-900 dark:text-white"> <h2 className="font-semibold text-neutral-900 dark:text-white">
{selectedVisitor ? 'Session Journey' : 'Select a visitor'} {selectedVisitor ? 'Session Journey' : 'Select a visitor'}

View File

@@ -396,7 +396,7 @@ export default function SiteSettingsPage() {
required required
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900 className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900
focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white" focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white"
/> />
</div> </div>
@@ -424,7 +424,7 @@ export default function SiteSettingsPage() {
type="text" type="text"
value={site.domain} value={site.domain}
disabled disabled
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-100 dark:bg-neutral-800/50 text-neutral-500 dark:text-neutral-400 cursor-not-allowed" className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-neutral-100 dark:bg-neutral-800/50 text-neutral-500 dark:text-neutral-400 cursor-not-allowed"
/> />
<p className="text-xs text-neutral-500 dark:text-neutral-400"> <p className="text-xs text-neutral-500 dark:text-neutral-400">
Domain cannot be changed after creation Domain cannot be changed after creation
@@ -495,7 +495,7 @@ export default function SiteSettingsPage() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<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> <div>
<h3 className="font-medium text-red-900 dark:text-red-200">Reset Data</h3> <h3 className="font-medium text-red-900 dark:text-red-200">Reset Data</h3>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">Delete all stats and events. This cannot be undone.</p> <p className="text-sm text-red-700 dark:text-red-300 mt-1">Delete all stats and events. This cannot be undone.</p>
@@ -508,7 +508,7 @@ export default function SiteSettingsPage() {
</button> </button>
</div> </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> <div>
<h3 className="font-medium text-red-900 dark:text-red-200">Delete Site</h3> <h3 className="font-medium text-red-900 dark:text-red-200">Delete Site</h3>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">Permanently delete this site and all data.</p> <p className="text-sm text-red-700 dark:text-red-300 mt-1">Permanently delete this site and all data.</p>
@@ -534,7 +534,7 @@ export default function SiteSettingsPage() {
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage who can view your dashboard.</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Manage who can view your dashboard.</p>
</div> </div>
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-white dark:bg-neutral-800 rounded-lg text-neutral-400"> <div className="p-2 bg-white dark:bg-neutral-800 rounded-lg text-neutral-400">
@@ -576,7 +576,7 @@ export default function SiteSettingsPage() {
type="text" type="text"
readOnly readOnly
value={`${APP_URL}/share/${siteId}`} value={`${APP_URL}/share/${siteId}`}
className="flex-1 px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 font-mono text-sm" className="flex-1 px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 font-mono text-sm"
/> />
<button <button
type="button" type="button"
@@ -673,7 +673,7 @@ export default function SiteSettingsPage() {
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Data Collection</h3> <h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Data Collection</h3>
{/* Page Paths Toggle */} {/* Page Paths Toggle */}
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Page Paths</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Page Paths</h4>
@@ -694,7 +694,7 @@ export default function SiteSettingsPage() {
</div> </div>
{/* Referrers Toggle */} {/* Referrers Toggle */}
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Referrers</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Referrers</h4>
@@ -715,7 +715,7 @@ export default function SiteSettingsPage() {
</div> </div>
{/* Device Info Toggle */} {/* Device Info Toggle */}
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Device Info</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Device Info</h4>
@@ -736,7 +736,7 @@ export default function SiteSettingsPage() {
</div> </div>
{/* Geographic Data Dropdown */} {/* Geographic Data Dropdown */}
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Geographic Data</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Geographic Data</h4>
@@ -760,7 +760,7 @@ export default function SiteSettingsPage() {
</div> </div>
{/* Screen Resolution Toggle */} {/* Screen Resolution Toggle */}
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Screen Resolution</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Screen Resolution</h4>
@@ -784,7 +784,7 @@ export default function SiteSettingsPage() {
{/* Bot and noise filtering */} {/* Bot and noise filtering */}
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800"> <div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Filtering</h3> <h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Filtering</h3>
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Filter bots and referrer spam</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Filter bots and referrer spam</h4>
@@ -808,7 +808,7 @@ export default function SiteSettingsPage() {
{/* Performance Insights Toggle */} {/* Performance Insights Toggle */}
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800"> <div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Performance Insights</h3> <h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Performance Insights</h3>
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800"> <div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className="font-medium text-neutral-900 dark:text-white">Performance Insights (Add-on)</h4> <h4 className="font-medium text-neutral-900 dark:text-white">Performance Insights (Add-on)</h4>
@@ -843,7 +843,7 @@ export default function SiteSettingsPage() {
value={formData.excluded_paths} value={formData.excluded_paths}
onChange={(e) => setFormData({ ...formData, excluded_paths: e.target.value })} onChange={(e) => setFormData({ ...formData, excluded_paths: e.target.value })}
placeholder="/admin/*&#10;/staging/*" placeholder="/admin/*&#10;/staging/*"
className="w-full px-4 py-3 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900 className="w-full px-4 py-3 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900
focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white font-mono text-sm" focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white font-mono text-sm"
/> />
</div> </div>
@@ -937,14 +937,14 @@ export default function SiteSettingsPage() {
)} )}
<div className="space-y-2"> <div className="space-y-2">
{goals.length === 0 ? ( {goals.length === 0 ? (
<div className="p-6 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-500 dark:text-neutral-400 text-sm"> <div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-500 dark:text-neutral-400 text-sm">
No goals yet. Add a goal to give custom events a display name in the dashboard. No goals yet. Add a goal to give custom events a display name in the dashboard.
</div> </div>
) : ( ) : (
goals.map((goal) => ( goals.map((goal) => (
<div <div
key={goal.id} key={goal.id}
className="flex items-center justify-between py-3 px-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50/50 dark:bg-neutral-900/50" className="flex items-center justify-between py-3 px-4 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50/50 dark:bg-neutral-900/50"
> >
<div> <div>
<span className="font-medium text-neutral-900 dark:text-white">{goal.name}</span> <span className="font-medium text-neutral-900 dark:text-white">{goal.name}</span>
@@ -994,7 +994,7 @@ export default function SiteSettingsPage() {
value={goalForm.name} value={goalForm.name}
onChange={(e) => setGoalForm({ ...goalForm, name: e.target.value })} onChange={(e) => setGoalForm({ ...goalForm, name: e.target.value })}
placeholder="e.g. Signups" placeholder="e.g. Signups"
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white" className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
required required
/> />
</div> </div>
@@ -1005,7 +1005,7 @@ export default function SiteSettingsPage() {
value={goalForm.event_name} value={goalForm.event_name}
onChange={(e) => setGoalForm({ ...goalForm, event_name: e.target.value })} onChange={(e) => setGoalForm({ ...goalForm, event_name: e.target.value })}
placeholder="e.g. signup_click (letters, numbers, underscores only)" placeholder="e.g. signup_click (letters, numbers, underscores only)"
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white" className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
required required
/> />
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">Only letters, numbers, and underscores; spaces become underscores. Invalid characters cannot be used. Max 64 characters after formatting.</p> <p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">Only letters, numbers, and underscores; spaces become underscores. Invalid characters cannot be used. Max 64 characters after formatting.</p>

View File

@@ -59,7 +59,7 @@ export default function NewSitePage() {
Create New Site Create New Site
</h1> </h1>
<form onSubmit={handleSubmit} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6"> <form onSubmit={handleSubmit} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<div className="mb-4"> <div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium mb-2 text-neutral-900 dark:text-white"> <label htmlFor="name" className="block text-sm font-medium mb-2 text-neutral-900 dark:text-white">
Site Name Site Name

View File

@@ -1,29 +1,239 @@
'use client'
import Link from 'next/link' 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 { interface FooterProps {
LinkComponent?: any LinkComponent?: any
appName?: string appName?: string
isAuthenticated?: boolean
} }
export function Footer({ LinkComponent = Link, appName = 'Pulse' }: FooterProps) { const footerLinks = {
const Component = LinkComponent 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 ( return (
<footer className="border-t border-neutral-200 dark:border-neutral-800 mt-auto bg-white dark:bg-neutral-950"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="mx-auto max-w-6xl px-4 sm:px-6 py-12 lg:py-16">
<div className="flex flex-col md:flex-row justify-between items-center gap-4"> {/* * Main footer content */}
<div className="text-sm text-neutral-500 dark:text-neutral-400"> <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">
© {new Date().getFullYear()} Ciphera. All rights reserved. {/* * 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>
<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"> {/* * Products */}
Why {appName} <div>
</Component> <h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Products</h4>
<Component href="/faq" className="hover:text-brand-orange transition-colors"> <ul className="space-y-3">
FAQ {footerLinks.products.map((link) => (
</Component> <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> </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>
</div> </div>
</footer> </footer>

View File

@@ -62,7 +62,7 @@ export default function PasswordInput({
onBlur={onBlur} onBlur={onBlur}
aria-invalid={!!error} aria-invalid={!!error}
aria-describedby={error ? errorId : undefined} 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 transition-all duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed dark:text-white
${error ${error
? 'border-red-300 dark:border-red-800 focus:border-red-500 focus:ring-4 focus:ring-red-500/10' ? '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 { useState, useEffect } from 'react'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import { motion } from 'framer-motion'
import { Button, CheckCircleIcon } from '@ciphera-net/ui' import { Button, CheckCircleIcon } from '@ciphera-net/ui'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { initiateOAuthFlow } from '@/lib/api/oauth' import { initiateOAuthFlow } from '@/lib/api/oauth'
@@ -212,17 +213,27 @@ export default function PricingSection() {
return ( return (
<section className="py-24 px-4 max-w-6xl mx-auto"> <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"> <h2 className="text-4xl md:text-5xl font-bold text-neutral-900 dark:text-white mb-6 tracking-tight">
Transparent Pricing Transparent Pricing
</h2> </h2>
<p className="text-xl text-neutral-600 dark:text-neutral-400"> <p className="text-xl text-neutral-600 dark:text-neutral-400">
Scale with your traffic. No hidden fees. Scale with your traffic. No hidden fees.
</p> </p>
</div> </motion.div>
{/* Unified Container */} {/* 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 */} {/* 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"> <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"> <div className="bg-neutral-200 dark:bg-neutral-800 p-1 rounded-lg flex">
<button <button
onClick={() => setIsYearly(false)} 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 !isYearly
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'
@@ -262,7 +273,7 @@ export default function PricingSection() {
</button> </button>
<button <button
onClick={() => setIsYearly(true)} 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 isYearly
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'
@@ -285,9 +296,9 @@ export default function PricingSection() {
{isTeam && ( {isTeam && (
<> <>
<div className="absolute top-0 left-0 w-full h-1 bg-brand-orange" /> <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 Most Popular
</div> </span>
</> </>
)} )}
@@ -331,11 +342,8 @@ export default function PricingSection() {
<Button <Button
onClick={() => handleSubscribe(plan.id)} onClick={() => handleSubscribe(plan.id)}
disabled={loadingPlan === plan.id || !!loadingPlan || !priceDetails} disabled={loadingPlan === plan.id || !!loadingPlan || !priceDetails}
className={`w-full mb-8 ${ variant={isTeam ? 'primary' : 'secondary'}
isTeam className="w-full mb-8"
? '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'
}`}
> >
{loadingPlan === plan.id ? 'Loading...' : !priceDetails ? 'Contact us' : 'Start free trial'} {loadingPlan === plan.id ? 'Loading...' : !priceDetails ? 'Contact us' : 'Start free trial'}
</Button> </Button>
@@ -383,7 +391,7 @@ export default function PricingSection() {
</ul> </ul>
</div> </div>
</div> </div>
</div> </motion.div>
</section> </section>
) )
} }

View File

@@ -53,7 +53,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
{/* {/*
<button <button
onClick={() => handleSwitch(null)} 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' !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 <button
key={org.organization_id} key={org.organization_id}
onClick={() => handleSwitch(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' 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 */} {/* Create New */}
<Link <Link
href="/onboarding" 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"> <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" /> <PlusIcon className="h-3 w-3" />

View File

@@ -65,7 +65,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
return ( 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"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Campaigns Campaigns
@@ -92,8 +92,9 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="space-y-2 flex-1 min-h-[270px] flex flex-col items-center justify-center"> <div className="space-y-2 flex-1 min-h-[270px] flex flex-col items-center justify-center gap-2">
<p className="text-neutral-500">Loading...</p> <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>
) : hasData ? ( ) : hasData ? (
<div className="space-y-2 flex-1 min-h-[270px]"> <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"> <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {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"> <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 const dayTicks = interval === 'day' && chartData.length > 0 ? chartData.map((c) => c.date) : undefined
return ( 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) */} {/* 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"> <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) => ( {metrics.map((item) => (

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { formatNumber } from '@/lib/utils/format' import { formatNumber } from '@/lib/utils/format'
import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats' 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 { interface ContentStatsProps {
topPages: TopPage[] topPages: TopPage[]
@@ -87,7 +87,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
return ( 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 justify-between mb-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <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 <button
key={tab} key={tab}
onClick={() => setActiveTab(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 activeTab === tab
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : '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"> <div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p> <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>
)} )}
</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"> <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {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) => ( (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"> <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 { formatNumber } from '@/lib/utils/format'
import * as Flags from 'country-flag-icons/react/3x2' import * as Flags from 'country-flag-icons/react/3x2'
import WorldMap from './WorldMap' import WorldMap from './WorldMap'
import { GlobeIcon } from '@ciphera-net/ui'
interface LocationProps { interface LocationProps {
countries: Array<{ country: string; pageviews: number }> countries: Array<{ country: string; pageviews: number }>
@@ -36,7 +37,19 @@ export default function Locations({ countries, cities }: LocationProps) {
const renderContent = () => { const renderContent = () => {
if (activeTab === 'countries') { if (activeTab === 'countries') {
if (!countries || countries.length === 0) { 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -60,7 +73,19 @@ export default function Locations({ countries, cities }: LocationProps) {
if (activeTab === 'cities') { if (activeTab === 'cities') {
if (!cities || cities.length === 0) { 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 ( return (
<div className="space-y-3"> <div className="space-y-3">
@@ -81,7 +106,7 @@ export default function Locations({ countries, cities }: LocationProps) {
} }
return ( 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"> <div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Locations 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"> <div className="flex p-1 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
<button <button
onClick={() => setActiveTab('countries')} 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' activeTab === 'countries'
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : '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>
<button <button
onClick={() => setActiveTab('cities')} 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' activeTab === 'cities'
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : '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 const hasData = list.length > 0
return ( 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"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Goals & Events Goals & Events

View File

@@ -187,7 +187,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
return ( 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 justify-between mb-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <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 <button
key={tab} key={tab}
onClick={() => setActiveTab(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 activeTab === tab
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : '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> </div>
) : activeTab === 'map' ? ( ) : activeTab === 'map' ? (
hasData ? <WorldMap data={filterUnknown(countries)} /> : ( hasData ? <WorldMap data={filterUnknown(countries)} /> : (
<div className="h-full flex flex-col items-center justify-center"> <div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p> <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>
) )
) : ( ) : (
@@ -252,14 +260,22 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
))} ))}
{Array.from({ length: emptySlots }).map((_, i) => ( {Array.from({ length: emptySlots }).map((_, i) => (
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" /> <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"> <div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p> <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> </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>
</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"> <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {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) => ( (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"> <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` const summaryText = `LCP ${Math.round(stats.lcp)} ms · CLS ${Number(stats.cls.toFixed(3))} · INP ${Math.round(stats.inp)} ms`
return ( 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. */} {/* * One-line summary: Performance score + metric summary. Click to expand. */}
<button <button
type="button" type="button"

View File

@@ -13,7 +13,7 @@ export default function RealtimeVisitors({ count, siteId }: RealtimeVisitorsProp
return ( return (
<div <div
onClick={() => siteId && router.push(`/sites/${siteId}/realtime`)} 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="flex items-center justify-between mb-2">
<div className="text-sm text-neutral-600 dark:text-neutral-400"> <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 { formatNumber } from '@/lib/utils/format'
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons' import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
import { MdMonitor } from 'react-icons/md' 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' import { getBrowsers, getOS, getDevices, getScreenResolutions } from '@/lib/api/stats'
interface TechSpecsProps { interface TechSpecsProps {
@@ -110,7 +110,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
return ( 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 justify-between mb-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <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 <button
key={tab} key={tab}
onClick={() => setActiveTab(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 activeTab === tab
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' ? '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' : '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"> <div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p> <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>
)} )}
</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"> <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {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) => ( (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"> <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' 'use client'
import { formatNumber } from '@/lib/utils/format' import { formatNumber } from '@/lib/utils/format'
import { LayoutDashboardIcon } from '@ciphera-net/ui'
interface TopPagesProps { interface TopPagesProps {
pages: Array<{ path: string; pageviews: number }> pages: Array<{ path: string; pageviews: number }>
@@ -9,17 +10,27 @@ interface TopPagesProps {
export default function TopPages({ pages }: TopPagesProps) { export default function TopPages({ pages }: TopPagesProps) {
if (!pages || pages.length === 0) { if (!pages || pages.length === 0) {
return ( 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"> <h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
Top Pages Top Pages
</h3> </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> </div>
) )
} }
return ( 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"> <h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
Top Pages Top Pages
</h3> </h3>

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { formatNumber } from '@/lib/utils/format' import { formatNumber } from '@/lib/utils/format'
import { getReferrerIcon } from '@/lib/utils/icons' 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' import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
interface TopReferrersProps { interface TopReferrersProps {
@@ -55,7 +55,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
return ( 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"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> <h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Top Referrers 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"> <div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<p className="text-neutral-600 dark:text-neutral-400">No data available</p> <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>
)} )}
</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"> <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {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) => ( (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"> <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

@@ -490,7 +490,7 @@ export default function OrganizationSettings() {
<p className="text-sm text-neutral-500 dark:text-neutral-400">Irreversible actions for this organization.</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Irreversible actions for this organization.</p>
</div> </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> <div>
<h3 className="font-medium text-red-900 dark:text-red-200">Delete Organization</h3> <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> <p className="text-sm text-red-700 dark:text-red-300 mt-1">Permanently delete this organization and all its data.</p>
@@ -513,7 +513,7 @@ export default function OrganizationSettings() {
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Organization Members</h2> <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> <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> <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"> <form onSubmit={handleSendInvite} className="flex gap-3 items-end">
<div className="flex-1"> <div className="flex-1">
@@ -558,7 +558,7 @@ export default function OrganizationSettings() {
{/* Members List */} {/* Members List */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider">Active Members</h3> <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 ? ( {isLoadingMembers ? (
<div className="p-8 text-center text-neutral-500"> <div className="p-8 text-center text-neutral-500">
<div className="animate-spin w-5 h-5 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-2"></div> <div className="animate-spin w-5 h-5 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-2"></div>
@@ -603,7 +603,7 @@ export default function OrganizationSettings() {
{invitations.length > 0 && ( {invitations.length > 0 && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider">Pending Invitations</h3> <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) => ( {invitations.map((invite) => (
<div key={invite.id} className="p-4 flex items-center justify-between"> <div key={invite.id} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -647,7 +647,7 @@ export default function OrganizationSettings() {
Loading subscription details... Loading subscription details...
</div> </div>
) : !subscription ? ( ) : !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> <p className="text-neutral-500">Could not load subscription details.</p>
<Button <Button
variant="ghost" variant="ghost"
@@ -660,7 +660,7 @@ export default function OrganizationSettings() {
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{/* Current Plan */} {/* 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 className="flex items-start justify-between mb-6">
<div> <div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-1">Current Plan</h3> <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-1">Current Plan</h3>
@@ -738,7 +738,7 @@ export default function OrganizationSettings() {
</div> </div>
{!subscription.has_payment_method && ( {!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> <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"> <p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
Get higher limits, more data retention, and priority support. Get higher limits, more data retention, and priority support.
@@ -752,7 +752,7 @@ export default function OrganizationSettings() {
{/* Invoice History */} {/* Invoice History */}
<div> <div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-4">Invoice History</h3> <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 ? ( {isLoadingInvoices ? (
<div className="p-8 text-center text-neutral-500"> <div className="p-8 text-center text-neutral-500">
<div className="animate-spin w-5 h-5 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-2"></div> <div className="animate-spin w-5 h-5 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-2"></div>
@@ -829,7 +829,7 @@ export default function OrganizationSettings() {
</div> </div>
{/* Advanced Filters */} {/* 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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-1"> <div className="space-y-1">
<label className="block text-xs font-medium text-neutral-500 uppercase">Log ID</label> <label className="block text-xs font-medium text-neutral-500 uppercase">Log ID</label>
@@ -889,7 +889,7 @@ export default function OrganizationSettings() {
</div> </div>
{/* Table */} {/* 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 ? ( {isLoadingAudit ? (
<div className="p-12 text-center text-neutral-500"> <div className="p-12 text-center text-neutral-500">
<div className="animate-spin w-6 h-6 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-3"></div> <div className="animate-spin w-6 h-6 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-3"></div>

View File

@@ -2,7 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import { Site } from '@/lib/api/sites' 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' import { useAuth } from '@/lib/auth/context'
interface SiteListProps { interface SiteListProps {
@@ -18,7 +18,7 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
return ( return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => ( {[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> </div>
) )
@@ -38,7 +38,7 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
{sites.map((site) => ( {sites.map((site) => (
<div <div
key={site.id} 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 */} {/* Header: Icon + Name + Live Status */}
<div className="flex items-start justify-between mb-6"> <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"> <div className="mt-auto flex gap-2">
<Link <Link
href={`/sites/${site.id}`} 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" /> <Button variant="primary" className="w-full justify-center text-sm">
View Dashboard <BarChartIcon className="w-4 h-4" />
View Dashboard
</Button>
</Link> </Link>
{(user?.role === 'owner' || user?.role === 'admin') && ( {(user?.role === 'owner' || user?.role === 'admin') && (
<button <button
@@ -114,7 +116,7 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) {
))} ))}
{/* Resources Card */} {/* 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"> <div className="mb-3 rounded-full bg-neutral-200 p-3 dark:bg-neutral-800">
<BookOpenIcon className="h-6 w-6 text-neutral-500" /> <BookOpenIcon className="h-6 w-6 text-neutral-500" />
</div> </div>

View File

@@ -196,7 +196,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
</div> </div>
{generatedUrl && ( {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> <code className="text-sm break-all text-brand-orange font-mono">{generatedUrl}</code>
<Button <Button
variant="secondary" variant="secondary"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -20,6 +20,8 @@
} }
@layer components { @layer components {
/* * TODO: Move these shared utilities to @ciphera-net/ui to avoid duplication with website */
/* * Glass Card Effect - Crucial for the "Premium" feel */ /* * Glass Card Effect - Crucial for the "Premium" feel */
.card-glass { .card-glass {
@apply bg-white/80 dark:bg-neutral-900/80; @apply bg-white/80 dark:bg-neutral-900/80;