[PULSE-43] Design system standardization and branding alignment #11
@@ -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 matter—visitors, pageviews, and sources—while respecting user privacy.
|
Pulse is different. We focus on the metrics that actually matter—visitors, pageviews, and sources—while 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">→</span>
|
View Guide <span aria-hidden="true">→</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>
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
82
app/page.tsx
82
app/page.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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/* /staging/*"
|
placeholder="/admin/* /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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 |
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user