public: new page and copy improvements
This commit is contained in:
@@ -1,31 +1,34 @@
|
||||
import { FeatureCardContainer } from '@/components/feature-card';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { ArrowRightIcon, type LucideIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { FeatureCardContainer } from '@/components/feature-card';
|
||||
|
||||
interface FeatureCardLinkProps {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: LucideIcon;
|
||||
}
|
||||
|
||||
export function FeatureCardLink({
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
}: FeatureCardLinkProps) {
|
||||
return (
|
||||
<Link href={url}>
|
||||
<FeatureCardContainer>
|
||||
<div className="row gap-3 items-center">
|
||||
<div className="col gap-1 flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors">
|
||||
<div className="row items-center gap-3">
|
||||
<div className="col min-w-0 flex-1 gap-1">
|
||||
{Icon && <Icon className="mb-2 size-6 shrink-0" />}
|
||||
<h3 className="font-semibold text-lg transition-colors group-hover:text-primary">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
<p className="line-clamp-2 text-muted-foreground text-sm">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="opacity-0 group-hover:opacity-100 size-5 shrink-0 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all duration-300" />
|
||||
<ArrowRightIcon className="size-5 shrink-0 text-muted-foreground opacity-0 transition-all duration-300 group-hover:translate-x-1 group-hover:text-primary group-hover:opacity-100" />
|
||||
</div>
|
||||
</FeatureCardContainer>
|
||||
</Link>
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
BellIcon,
|
||||
ConeIcon,
|
||||
DollarSignIcon,
|
||||
FilterIcon,
|
||||
GlobeIcon,
|
||||
MonitorIcon,
|
||||
MousePointerClickIcon,
|
||||
PieChartIcon,
|
||||
RefreshCwIcon,
|
||||
ShareIcon,
|
||||
UserIcon,
|
||||
WorkflowIcon,
|
||||
} from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import { FeatureCardLink } from './_components/feature-card';
|
||||
import { FeatureHero } from '@/app/(content)/features/[slug]/_components/feature-hero';
|
||||
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
@@ -5,8 +22,21 @@ import { WindowImage } from '@/components/window-image';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
|
||||
import { featureSource } from '@/lib/source';
|
||||
import type { Metadata } from 'next';
|
||||
import { FeatureCardLink } from './_components/feature-card';
|
||||
|
||||
const featureIcons: Record<string, LucideIcon> = {
|
||||
conversion: FilterIcon,
|
||||
'data-visualization': PieChartIcon,
|
||||
'event-tracking': MousePointerClickIcon,
|
||||
funnels: ConeIcon,
|
||||
'identify-users': UserIcon,
|
||||
integrations: WorkflowIcon,
|
||||
notifications: BellIcon,
|
||||
retention: RefreshCwIcon,
|
||||
'revenue-tracking': DollarSignIcon,
|
||||
'session-tracking': MonitorIcon,
|
||||
'share-and-collaborate': ShareIcon,
|
||||
'web-analytics': GlobeIcon,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = getPageMetadata({
|
||||
title: 'Product analytics features',
|
||||
@@ -32,36 +62,37 @@ export default async function FeaturesIndexPage() {
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
srcDark="/screenshots/overview-dark.webp"
|
||||
srcLight="/screenshots/overview-light.webp"
|
||||
alt="OpenPanel Dashboard Overview"
|
||||
caption="Get a clear view of your product analytics with real-time insights and customizable dashboards."
|
||||
srcDark="/screenshots/overview-dark.webp"
|
||||
srcLight="/screenshots/overview-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
title="All features"
|
||||
description="Browse our capabilities. Each feature is designed to answer specific questions about your product and users."
|
||||
title="All features"
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-12">
|
||||
<div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 2xl:grid-cols-3">
|
||||
{features.map((feature) => (
|
||||
<FeatureCardLink
|
||||
key={feature.slug}
|
||||
url={feature.url}
|
||||
title={feature.hero.heading}
|
||||
description={feature.hero.subheading}
|
||||
icon={featureIcons[feature.slug]}
|
||||
key={feature.slug}
|
||||
title={feature.hero.heading}
|
||||
url={feature.url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<CtaBanner
|
||||
title="Ready to get started?"
|
||||
description="Join thousands of teams using OpenPanel for their analytics needs."
|
||||
ctaText="Get Started Free"
|
||||
ctaLink="https://dashboard.openpanel.dev/onboarding"
|
||||
ctaText="Get Started Free"
|
||||
description="Join thousands of teams using OpenPanel for their analytics needs."
|
||||
title="Ready to get started?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
574
apps/public/src/app/(content)/open-source-analytics/page.tsx
Normal file
574
apps/public/src/app/(content)/open-source-analytics/page.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
BarChart3Icon,
|
||||
CalendarIcon,
|
||||
CookieIcon,
|
||||
GithubIcon,
|
||||
InfinityIcon,
|
||||
LayersIcon,
|
||||
LineChartIcon,
|
||||
RefreshCwIcon,
|
||||
RocketIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
UnlockIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import Script from 'next/script';
|
||||
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
|
||||
import { HeroContainer } from '@/app/(home)/_sections/hero';
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { DataOwnershipIllustration } from '@/components/illustrations/data-ownership';
|
||||
import { PrivacyIllustration } from '@/components/illustrations/privacy';
|
||||
import { ProductAnalyticsIllustration } from '@/components/illustrations/product-analytics';
|
||||
import { WebAnalyticsIllustration } from '@/components/illustrations/web-analytics';
|
||||
import { Perks } from '@/components/perks';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { Testimonial } from '@/components/testimonial';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { WindowImage } from '@/components/window-image';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const metadata: Metadata = getPageMetadata({
|
||||
title: 'Open Source Analytics | Web & Product Analytics Platform',
|
||||
description:
|
||||
'OpenPanel is an open source analytics platform for web and product teams. Privacy-first, cookieless, self-hostable. Combine web analytics and product analytics in one tool. Free trial.',
|
||||
url: url('/open-source-analytics'),
|
||||
image: getOgImageUrl('/open-source-analytics'),
|
||||
});
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: 'Open Source Analytics | Web & Product Analytics Platform | OpenPanel',
|
||||
description:
|
||||
'OpenPanel is an open source analytics platform for web and product teams. Privacy-first, cookieless, self-hostable. Combine web analytics and product analytics in one tool.',
|
||||
url: url('/open-source-analytics'),
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'OpenPanel',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: url('/logo.png'),
|
||||
},
|
||||
},
|
||||
mainEntity: {
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'OpenPanel',
|
||||
applicationCategory: 'AnalyticsApplication',
|
||||
operatingSystem: 'Web',
|
||||
url: url('/'),
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
description: 'Free 30-day trial, self-host for free',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const heroPerks = [
|
||||
{ text: 'Open source (AGPL-3.0)', icon: GithubIcon },
|
||||
{ text: 'Self-hostable', icon: ServerIcon },
|
||||
{ text: 'Cookieless tracking', icon: CookieIcon },
|
||||
{ text: 'GDPR compliant', icon: ShieldCheckIcon },
|
||||
{ text: 'Web + product analytics', icon: BarChart3Icon },
|
||||
{ text: '30-day free trial', icon: CalendarIcon },
|
||||
];
|
||||
|
||||
export default function OpenSourceAnalyticsPage() {
|
||||
return (
|
||||
<div>
|
||||
<Script
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
id="open-source-analytics-schema"
|
||||
strategy="beforeInteractive"
|
||||
type="application/ld+json"
|
||||
/>
|
||||
|
||||
{/* Hero */}
|
||||
<HeroContainer className="-mb-32 max-sm:**:data-children:pb-0">
|
||||
<div className="col w-full gap-8 sm:w-1/2 sm:pr-12">
|
||||
<div className="col gap-4">
|
||||
<h1 className="font-semibold text-4xl leading-[1.1] md:text-5xl">
|
||||
Open Source Analytics for Web and Product Teams
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
OpenPanel is an open source analytics platform that combines web
|
||||
analytics and product analytics in one privacy-first tool. Track
|
||||
pageviews, events, funnels, retention, and user journeys — all
|
||||
without cookies.
|
||||
</p>
|
||||
</div>
|
||||
<div className="row gap-4">
|
||||
<GetStartedButton text="Start free trial" />
|
||||
<Button asChild className="px-6" size="lg" variant="outline">
|
||||
<Link href="/docs/self-hosting/self-hosting">
|
||||
Self-host for free
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Perks perks={heroPerks} />
|
||||
</div>
|
||||
|
||||
<div className="col group relative max-sm:px-4 sm:w-1/2">
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden rounded-lg border border-border bg-background shadow-lg',
|
||||
'sm:absolute sm:-top-12 sm:-bottom-64 sm:left-0 sm:w-[800px]',
|
||||
'relative max-sm:-mx-4 max-sm:mt-12 max-sm:h-[800px]'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-12 items-center gap-2 border-border border-b bg-muted/50 px-4">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<a
|
||||
className="group flex flex-1 items-center gap-2 rounded-md border border-border bg-background/20 px-3 py-1 text-sm"
|
||||
href="https://demo.openpanel.dev/demo/shoey/dashboards/e-commerce"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="flex-1 text-muted-foreground">
|
||||
demo.openpanel.dev
|
||||
</span>
|
||||
<ArrowRightIcon className="size-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</a>
|
||||
</div>
|
||||
<iframe
|
||||
className="h-full w-full"
|
||||
scrolling="no"
|
||||
src="https://demo.openpanel.dev/demo/shoey/dashboards/e-commerce"
|
||||
title="OpenPanel e-commerce analytics dashboard demo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HeroContainer>
|
||||
|
||||
<div className="container">
|
||||
<div className="col gap-0">
|
||||
{/* Web + Product Analytics in One Platform */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
description="Most open source analytics tools focus on either web analytics or product analytics. OpenPanel does both — so you don't need to run two separate tools."
|
||||
title="Open source web analytics and product analytics in one platform"
|
||||
/>
|
||||
<p className="mt-4 max-w-3xl text-muted-foreground">
|
||||
Track pageviews and traffic sources alongside user events,
|
||||
funnels, retention curves, and individual user journeys. One
|
||||
platform, one tracking snippet, one dashboard.
|
||||
</p>
|
||||
<div className="mt-8 mb-6 grid gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Understand your website performance with privacy-first analytics and clear, actionable insights."
|
||||
illustration={<WebAnalyticsIllustration />}
|
||||
title="Web Analytics"
|
||||
variant="large"
|
||||
/>
|
||||
<FeatureCard
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Turn raw data into clarity with real-time visualization of performance, behavior, and trends."
|
||||
illustration={<ProductAnalyticsIllustration />}
|
||||
title="Product Analytics"
|
||||
variant="large"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
description="See visitors, pageviews, referrers, countries, devices, and browsers — updated in real time with no delays."
|
||||
icon={LineChartIcon}
|
||||
link={{
|
||||
href: '/features/web-analytics',
|
||||
children: 'Learn about web analytics',
|
||||
}}
|
||||
title="Real-time Web Analytics"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Build conversion funnels to see where users drop off. Identify bottlenecks and optimize your user flow."
|
||||
icon={LayersIcon}
|
||||
link={{
|
||||
href: '/features/funnels',
|
||||
children: 'Learn about funnels',
|
||||
}}
|
||||
title="Funnels"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Measure how many users come back over time. Understand engagement patterns and improve long-term retention."
|
||||
icon={RefreshCwIcon}
|
||||
link={{
|
||||
href: '/features/retention',
|
||||
children: 'Learn about retention',
|
||||
}}
|
||||
title="Retention Analysis"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Track any custom event — signups, purchases, button clicks, form submissions. Flexible and schema-free."
|
||||
icon={BarChart3Icon}
|
||||
link={{
|
||||
href: '/features/event-tracking',
|
||||
children: 'Learn about event tracking',
|
||||
}}
|
||||
title="Event Tracking"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="See individual user journeys, session timelines, and profile data. Understand how specific users interact with your product."
|
||||
icon={UserIcon}
|
||||
link={{
|
||||
href: '/features/identify-users',
|
||||
children: 'Learn about user profiles',
|
||||
}}
|
||||
title="User Profiles & Sessions"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="SDKs for React, Next.js, Vue, Astro, Swift, Kotlin, Python, and more. Add tracking with a few lines of code."
|
||||
icon={GithubIcon}
|
||||
link={{
|
||||
href: '/docs/sdks',
|
||||
children: 'Browse all SDKs',
|
||||
}}
|
||||
title="15+ SDKs & Integrations"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Testimonial: Data ownership */}
|
||||
<Testimonial
|
||||
author="Self-hosting users"
|
||||
className="my-16"
|
||||
quote={
|
||||
'\u201COpenPanel gives us the same, in many ways better, analytics while keeping full ownership of our data. It\u2019s truly self-hosted but surprisingly low maintenance.\u201D'
|
||||
}
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<SectionHeader
|
||||
description="Open source analytics tools give you full transparency and control over how your data is collected, stored, and analyzed."
|
||||
title="What is open source analytics?"
|
||||
/>
|
||||
<div className="col prose prose-neutral dark:prose-invert mt-8 max-w-3xl gap-6">
|
||||
<p>
|
||||
Open source analytics means the source code of the analytics
|
||||
platform is publicly available for anyone to inspect, modify,
|
||||
and self-host. Unlike proprietary tools like{' '}
|
||||
<Link href="/compare/google-analytics-alternative">
|
||||
Google Analytics
|
||||
</Link>{' '}
|
||||
or <Link href="/compare/mixpanel-alternative">Mixpanel</Link>,
|
||||
open source analytics tools don't lock you into a vendor's
|
||||
ecosystem or force you to send your users' data to third-party
|
||||
servers.
|
||||
</p>
|
||||
<p>
|
||||
With open source analytics, you can audit exactly what data is
|
||||
being collected, verify there are no hidden trackers, deploy the
|
||||
software on your own infrastructure, and customize it to fit
|
||||
your needs. This matters for privacy-conscious teams, companies
|
||||
operating under GDPR or CCPA, and anyone who wants full
|
||||
ownership of their analytics data.
|
||||
</p>
|
||||
<p>
|
||||
The open source analytics ecosystem has matured significantly.
|
||||
Tools like OpenPanel, Plausible, PostHog, Matomo, and Umami
|
||||
offer production-ready alternatives to proprietary platforms —
|
||||
with the added benefits of transparency, self-hosting, and
|
||||
community-driven development.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Screenshot: Overview */}
|
||||
<div className="my-16">
|
||||
<WindowImage
|
||||
alt="OpenPanel open source analytics dashboard showing real-time web analytics overview"
|
||||
caption="Get instant insights into your traffic, top pages, referrers, and user behavior — all from one dashboard."
|
||||
srcDark="/screenshots/overview-dark.webp"
|
||||
srcLight="/screenshots/overview-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Why Open Source Analytics Matters */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
description="Proprietary analytics tools come with trade-offs that open source eliminates."
|
||||
title="Why open source analytics matters"
|
||||
/>
|
||||
<div className="mt-8 mb-6 grid gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Privacy-first analytics without cookies, fingerprinting, or invasive tracking. Built for compliance and user trust."
|
||||
illustration={<PrivacyIllustration />}
|
||||
title="Privacy-first"
|
||||
variant="large"
|
||||
/>
|
||||
<FeatureCard
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="You own your data — no vendors, no sharing, no hidden processing. Store analytics on your own infrastructure and stay in full control."
|
||||
illustration={<DataOwnershipIllustration />}
|
||||
title="Data Ownership"
|
||||
variant="large"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
description="Export your data anytime. Switch providers without losing history. No proprietary formats, no walled gardens, no enterprise contracts locking you in."
|
||||
icon={UnlockIcon}
|
||||
title="No Vendor Lock-in"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Transparent roadmap, public code, and community contributions. Report bugs, request features, or contribute directly. The project evolves based on real user needs."
|
||||
icon={UsersIcon}
|
||||
title="Community-Driven"
|
||||
>
|
||||
<Link
|
||||
className="text-muted-foreground text-sm transition-colors hover:text-primary"
|
||||
href="https://github.com/Openpanel-dev/openpanel"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
View on GitHub
|
||||
</Link>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Screenshot: Dashboard */}
|
||||
<div className="my-16">
|
||||
<WindowImage
|
||||
alt="OpenPanel custom analytics dashboard with charts and visualizations"
|
||||
caption="Build custom dashboards with the metrics that matter most to your team."
|
||||
srcDark="/screenshots/dashboard-dark.webp"
|
||||
srcLight="/screenshots/dashboard-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Testimonial: Dashboards & data */}
|
||||
<Testimonial
|
||||
author="Self-hosting users"
|
||||
className="my-16"
|
||||
quote={
|
||||
'\u201CThe dashboards are clear, the data is reliable, and the feature set covers everything we relied on before. We honestly don\u2019t want to run any business without OpenPanel anymore.\u201D'
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Self-Hosted Analytics */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
description="Run OpenPanel on your own servers with full data sovereignty. The self-hosted version is identical to the cloud — same features, no limitations."
|
||||
title="Self-hosted analytics: deploy on your own infrastructure"
|
||||
/>
|
||||
<div className="col prose prose-neutral dark:prose-invert mt-8 max-w-3xl gap-6">
|
||||
<p>
|
||||
Self-hosting gives you complete control. Your analytics data
|
||||
never leaves your infrastructure — ideal for companies with
|
||||
strict compliance requirements, healthcare organizations, or
|
||||
anyone who values data sovereignty. A mid-range VPS with 4 vCPU,
|
||||
8 GB RAM, and an SSD running Docker Compose is enough for most
|
||||
projects.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
description="One Docker Compose command to get up and running. No complex Kubernetes setup, no DevOps expertise required."
|
||||
icon={RocketIcon}
|
||||
title="Deploy in Minutes"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Self-hosted OpenPanel has no caps on events, users, dashboards, or data retention. Scale as much as your server allows."
|
||||
icon={InfinityIcon}
|
||||
title="No Event Limits"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Pull the latest version to get new features, bug fixes, and security patches. Stay current with minimal effort."
|
||||
icon={RefreshCwIcon}
|
||||
title="Always Up to Date"
|
||||
/>
|
||||
</div>
|
||||
<div className="row mt-8 gap-4">
|
||||
<GetStartedButton text="Start free cloud trial" />
|
||||
<Button asChild size="lg" variant="outline">
|
||||
<Link href="/docs/self-hosting/self-hosting">
|
||||
Read self-hosting docs
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Testimonial: Support */}
|
||||
<Testimonial
|
||||
author="Self-hosting users"
|
||||
className="my-16"
|
||||
quote={
|
||||
'\u201CThe support is absolutely fantastic: responsive, helpful, and knowledgeable, and that made the switch effortless. We spend less time managing tooling and more time acting on insights.\u201D'
|
||||
}
|
||||
/>
|
||||
|
||||
{/* How OpenPanel Compares */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
description="OpenPanel sits at the intersection of simple web analytics and powerful product analytics."
|
||||
title="How OpenPanel compares to other open source analytics tools"
|
||||
/>
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
description="Plausible is great for simple pageview analytics. OpenPanel adds product analytics — funnels, retention, user profiles, and event tracking — while keeping the same privacy-first approach."
|
||||
icon={BarChart3Icon}
|
||||
link={{
|
||||
href: '/compare/plausible-alternative',
|
||||
children: 'Full Plausible comparison',
|
||||
}}
|
||||
title="OpenPanel vs Plausible"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="PostHog is a powerful all-in-one platform. OpenPanel is more affordable once you exceed the free tier, simpler to self-host, and focused on analytics without the complexity."
|
||||
icon={BarChart3Icon}
|
||||
link={{
|
||||
href: '/compare/posthog-alternative',
|
||||
children: 'Full PostHog comparison',
|
||||
}}
|
||||
title="OpenPanel vs PostHog"
|
||||
/>
|
||||
<FeatureCard
|
||||
description="Mixpanel is proprietary and expensive at scale. OpenPanel is open source with similar product analytics capabilities at a fraction of the cost — and you can self-host for free."
|
||||
icon={BarChart3Icon}
|
||||
link={{
|
||||
href: '/compare/mixpanel-alternative',
|
||||
children: 'Full Mixpanel comparison',
|
||||
}}
|
||||
title="OpenPanel vs Mixpanel"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-8 max-w-3xl text-muted-foreground">
|
||||
Want a deeper look? Read our{' '}
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
href="/articles/open-source-web-analytics"
|
||||
>
|
||||
comprehensive comparison of 9 open source analytics tools
|
||||
</Link>{' '}
|
||||
with pricing tables, feature breakdowns, and honest reviews.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
{/* Get Started */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
description="From zero to tracking in minutes. Choose cloud or self-hosted — both give you the full platform."
|
||||
title="Get started with open source analytics"
|
||||
/>
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-3">
|
||||
<div className="col gap-3">
|
||||
<div className="center-center size-10 rounded-full bg-primary/10 font-semibold text-primary">
|
||||
1
|
||||
</div>
|
||||
<h3 className="font-semibold">Sign up or self-host</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Create a free cloud account or{' '}
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
href="/docs/self-hosting/self-hosting"
|
||||
>
|
||||
deploy with Docker Compose
|
||||
</Link>{' '}
|
||||
on your own server.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col gap-3">
|
||||
<div className="center-center size-10 rounded-full bg-primary/10 font-semibold text-primary">
|
||||
2
|
||||
</div>
|
||||
<h3 className="font-semibold">Add a few lines of code</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Install an{' '}
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
href="/docs/sdks"
|
||||
>
|
||||
SDK for your framework
|
||||
</Link>{' '}
|
||||
— React, Next.js, Vue, Astro, Python, and more.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col gap-3">
|
||||
<div className="center-center size-10 rounded-full bg-primary/10 font-semibold text-primary">
|
||||
3
|
||||
</div>
|
||||
<h3 className="font-semibold">Understand your users</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
See real-time data in minutes. Build dashboards, track
|
||||
conversions, and explore{' '}
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
href="/features"
|
||||
>
|
||||
all features
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<GetStartedButton text="Start free trial" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* FAQ */}
|
||||
<Section>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
description="Common questions from teams evaluating open source analytics platforms."
|
||||
title="Frequently asked questions about open source analytics"
|
||||
/>
|
||||
<Faqs>
|
||||
<FaqItem question="What is the best open source analytics tool?">
|
||||
{
|
||||
'The best open source analytics tool depends on your needs. **OpenPanel** is the best choice for teams that need both web analytics and product analytics in one platform. **PostHog** is ideal for engineering teams wanting analytics plus feature flags and experiments. **Plausible** is best for simple, privacy-first pageview tracking. **Matomo** is the most mature Google Analytics replacement. See our [full comparison of 9 open source analytics tools](/articles/open-source-web-analytics) for a detailed breakdown.'
|
||||
}
|
||||
</FaqItem>
|
||||
<FaqItem question="Is open source analytics free to self-host?">
|
||||
{
|
||||
'Yes. OpenPanel, PostHog, Plausible, Matomo, and Umami all offer free self-hosting. Your only cost is the server infrastructure — typically $20-50/month for a VPS that can handle millions of events. OpenPanel also offers a [cloud plan](/pricing) starting at $2.50/month with a 30-day free trial if you prefer not to manage servers.'
|
||||
}
|
||||
</FaqItem>
|
||||
<FaqItem question="Is OpenPanel GDPR compliant?">
|
||||
{
|
||||
'Yes. OpenPanel uses cookieless tracking by design — no cookies are set, no personal data is collected without explicit consent, and no consent banners are needed. This makes OpenPanel compliant with GDPR, CCPA, and PECR out of the box. Self-hosting gives you additional control by keeping all data on your own infrastructure within your preferred jurisdiction.'
|
||||
}
|
||||
</FaqItem>
|
||||
<FaqItem question="Can OpenPanel replace Google Analytics?">
|
||||
{`Yes. OpenPanel tracks all the metrics Google Analytics provides — pageviews, sessions, referrers, countries, devices, and more — plus product analytics features that GA doesn't offer, like funnels, retention, and individual user journeys. See our [detailed Google Analytics comparison](/compare/google-analytics-alternative) for a full breakdown.`}
|
||||
</FaqItem>
|
||||
<FaqItem question="What's the difference between web analytics and product analytics?">
|
||||
{
|
||||
'**Web analytics** focuses on website traffic: pageviews, visitors, referrers, bounce rates, and geographic data. **Product analytics** goes deeper into user behavior: event tracking, conversion funnels, retention curves, and individual user journeys. Most tools offer one or the other. OpenPanel combines both in a single platform, so you get the full picture without running separate tools.'
|
||||
}
|
||||
</FaqItem>
|
||||
<FaqItem question="How does OpenPanel compare to Plausible and Matomo?">
|
||||
{`**Plausible** is the simplest option — lightweight pageview analytics with no complexity. **Matomo** is the most mature — a full Google Analytics replacement with heatmaps and session recordings (paid plugins). **OpenPanel** sits in between: it's as easy to set up as Plausible but adds product analytics features like funnels, retention, and user profiles that Plausible doesn't offer, at a lower cost than Matomo Cloud. See our [Plausible comparison](/compare/plausible-alternative) and [Matomo comparison](/compare/matomo-alternative) for details.`}
|
||||
</FaqItem>
|
||||
<FaqItem question="Does OpenPanel use cookies?">
|
||||
{`No. OpenPanel uses cookieless tracking by default. No cookies are set on your visitors' browsers, which means you don't need cookie consent banners. This improves both user experience and data accuracy, since tracking isn't affected by cookie blockers or consent rejections.`}
|
||||
</FaqItem>
|
||||
</Faqs>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<CtaBanner
|
||||
description="Join thousands of teams using OpenPanel. Free 30-day trial, no credit card required. Self-host for free or use our cloud."
|
||||
title="Start tracking with open source analytics today"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import {
|
||||
BarChart3Icon,
|
||||
ChevronRightIcon,
|
||||
@@ -7,8 +5,10 @@ import {
|
||||
GlobeIcon,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { ProductAnalyticsIllustration } from './illustrations/product-analytics';
|
||||
import { WebAnalyticsIllustration } from './illustrations/web-analytics';
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { ProductAnalyticsIllustration } from '@/components/illustrations/product-analytics';
|
||||
import { WebAnalyticsIllustration } from '@/components/illustrations/web-analytics';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -47,42 +47,42 @@ export function AnalyticsInsights() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
description="Combine web and product analytics in one platform. Track visitors, events, revenue, and user journeys, all with privacy-first tracking."
|
||||
label="ANALYTICS & INSIGHTS"
|
||||
title="See the full picture of your users and product performance"
|
||||
description="Combine web and product analytics in one platform. Track visitors, events, revenue, and user journeys, all with privacy-first tracking."
|
||||
className="mb-16"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div className="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Web Analytics"
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Understand your website performance with privacy-first analytics and clear, actionable insights."
|
||||
illustration={<WebAnalyticsIllustration />}
|
||||
className="px-0 **:data-content:px-6"
|
||||
title="Web Analytics"
|
||||
variant="large"
|
||||
/>
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Product Analytics"
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Turn raw data into clarity with real-time visualization of performance, behavior, and trends."
|
||||
illustration={<ProductAnalyticsIllustration />}
|
||||
className="px-0 **:data-content:px-6"
|
||||
title="Product Analytics"
|
||||
variant="large"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{features.map((feature) => (
|
||||
<FeatureCard
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
description={feature.description}
|
||||
icon={feature.icon}
|
||||
key={feature.title}
|
||||
link={feature.link}
|
||||
title={feature.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-8 text-center">
|
||||
<Link
|
||||
className="inline-flex items-center gap-1 text-muted-foreground text-sm transition-colors hover:text-foreground"
|
||||
href="/features"
|
||||
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1 transition-colors"
|
||||
>
|
||||
Explore all features
|
||||
<ChevronRightIcon className="size-3.5" />
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { BoltIcon, GithubIcon, ServerIcon } from 'lucide-react';
|
||||
import { DataOwnershipIllustration } from './illustrations/data-ownership';
|
||||
import { PrivacyIllustration } from './illustrations/privacy';
|
||||
import Link from 'next/link';
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { DataOwnershipIllustration } from '@/components/illustrations/data-ownership';
|
||||
import { PrivacyIllustration } from '@/components/illustrations/privacy';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const secondaryFeatures = [
|
||||
{
|
||||
@@ -29,6 +32,7 @@ export function DataPrivacy() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description="OpenPanel gives you analytics on your terms - privacy-friendly, open-source, and fully self-hostable. Every part of the platform is designed to put you in control of your data while delivering fast, reliable insights without compromising user trust."
|
||||
title={
|
||||
<>
|
||||
Built for Control,
|
||||
@@ -36,32 +40,37 @@ export function DataPrivacy() {
|
||||
Transparency & Trust
|
||||
</>
|
||||
}
|
||||
description="OpenPanel gives you analytics on your terms - privacy-friendly, open-source, and fully self-hostable. Every part of the platform is designed to put you in control of your data while delivering fast, reliable insights without compromising user trust."
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6 mt-16">
|
||||
<div className="mt-16 mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Privacy-first"
|
||||
description="Privacy-first analytics without cookies, fingerprinting, or invasive tracking. Built for compliance and user trust."
|
||||
illustration={<PrivacyIllustration />}
|
||||
title="Privacy-first"
|
||||
variant="large"
|
||||
/>
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Data Ownership"
|
||||
description="You own your data - no vendors, no sharing, no hidden processing. Store analytics on your own infrastructure and stay in full control."
|
||||
illustration={<DataOwnershipIllustration />}
|
||||
title="Data Ownership"
|
||||
variant="large"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{secondaryFeatures.map((feature) => (
|
||||
<FeatureCard
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
description={feature.description}
|
||||
icon={feature.icon}
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="row mt-8 gap-4">
|
||||
<GetStartedButton />
|
||||
<Button asChild className="px-6" size="lg" variant="outline">
|
||||
<Link href="/docs/self-hosting/self-hosting">Self-host for free</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
|
||||
const faqData = [
|
||||
@@ -63,11 +64,13 @@ export function Faq() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
description="Some of the most common questions we get asked."
|
||||
title="FAQ"
|
||||
/>
|
||||
<div className="col gap-8">
|
||||
<SectionHeader
|
||||
description="Some of the most common questions we get asked."
|
||||
title="FAQ"
|
||||
/>
|
||||
<GetStartedButton className="w-fit max-md:hidden" />
|
||||
</div>
|
||||
<Faqs>
|
||||
{faqData.map((faq) => (
|
||||
<FaqItem key={faq.question} question={faq.question}>
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
'use client';
|
||||
import { Competition } from '@/components/competition';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Perks } from '@/components/perks';
|
||||
import { Tag } from '@/components/tag';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
CalendarIcon,
|
||||
ChevronRightIcon,
|
||||
CookieIcon,
|
||||
CreditCardIcon,
|
||||
DatabaseIcon,
|
||||
FlaskRoundIcon,
|
||||
GithubIcon,
|
||||
ServerIcon,
|
||||
StarIcon,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { Competition } from '@/components/competition';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Perks } from '@/components/perks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const perks = [
|
||||
{ text: 'Free trial 30 days', icon: CalendarIcon },
|
||||
@@ -40,46 +36,46 @@ function HeroImage({ className }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, x: 0 }}
|
||||
animate={
|
||||
isLoaded
|
||||
? { opacity: 0.5, scale: 1, x: 0 }
|
||||
: { opacity: 0, scale: 0.9, x: 0 }
|
||||
}
|
||||
transition={{
|
||||
duration: 2,
|
||||
}}
|
||||
className={cn('absolute', className)}
|
||||
initial={{ opacity: 0, scale: 0.9, x: 0 }}
|
||||
style={{
|
||||
left: `calc(50% - ${width / 2}px - 50px)`,
|
||||
top: -270,
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/hero-dark.webp"
|
||||
alt="Hero"
|
||||
width={width}
|
||||
height={height}
|
||||
className="hidden dark:block"
|
||||
height={height}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
src="/hero-dark.webp"
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
height,
|
||||
}}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
width={width}
|
||||
/>
|
||||
<Image
|
||||
src="/hero-light.webp"
|
||||
alt="Hero"
|
||||
width={width}
|
||||
height={height}
|
||||
className="dark:hidden"
|
||||
height={height}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
src="/hero-light.webp"
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
height,
|
||||
}}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
width={width}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -88,12 +84,12 @@ function HeroImage({ className }: { className?: string }) {
|
||||
export function Hero() {
|
||||
return (
|
||||
<HeroContainer className="-mb-32 max-sm:**:data-children:pb-0">
|
||||
<div className="col gap-8 w-full sm:w-1/2 sm:pr-12">
|
||||
<div className="col w-full gap-8 sm:w-1/2 sm:pr-12">
|
||||
<div className="col gap-4">
|
||||
{/* <div className="font-mono text-sm text-muted-foreground">
|
||||
TRUSTED BY 1,000+ COMPANIES • 4.7K GITHUB STARS
|
||||
</div> */}
|
||||
<h1 className="text-4xl md:text-5xl font-semibold leading-[1.1]">
|
||||
<div className="font-mono text-muted-foreground text-sm">
|
||||
TRUSTED BY 1,000+ PROJECTS
|
||||
</div>
|
||||
<h1 className="font-semibold text-4xl leading-[1.1] md:text-5xl">
|
||||
OpenPanel - The open-source alternative to <Competition />
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
@@ -104,7 +100,7 @@ export function Hero() {
|
||||
</div>
|
||||
<div className="row gap-4">
|
||||
<GetStartedButton />
|
||||
<Button size="lg" variant="outline" asChild className="px-6">
|
||||
<Button asChild className="px-6" size="lg" variant="outline">
|
||||
<Link
|
||||
href="https://demo.openpanel.dev/demo/shoey"
|
||||
rel="noreferrer noopener nofollow"
|
||||
@@ -118,39 +114,39 @@ export function Hero() {
|
||||
<Perks perks={perks} />
|
||||
</div>
|
||||
|
||||
<div className="col sm:w-1/2 relative group max-sm:px-4">
|
||||
<div className="col group relative max-sm:px-4 sm:w-1/2">
|
||||
<div
|
||||
className={cn([
|
||||
'overflow-hidden rounded-lg border border-border bg-background shadow-lg',
|
||||
'sm:absolute sm:left-0 sm:-top-12 sm:w-[800px] sm:-bottom-64',
|
||||
'max-sm:h-[800px] max-sm:-mx-4 max-sm:mt-12 relative',
|
||||
'sm:absolute sm:-top-12 sm:-bottom-64 sm:left-0 sm:w-[800px]',
|
||||
'relative max-sm:-mx-4 max-sm:mt-12 max-sm:h-[800px]',
|
||||
])}
|
||||
>
|
||||
{/* Window controls */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-muted/50 h-12">
|
||||
<div className="flex h-12 items-center gap-2 border-border border-b bg-muted/50 px-4 py-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-red-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
{/* URL bar */}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className="group mx-4 flex flex-1 items-center gap-2 rounded-md border border-border bg-background/20 px-3 py-1 text-sm"
|
||||
href="https://demo.openpanel.dev/demo/shoey"
|
||||
className="group flex-1 mx-4 px-3 py-1 text-sm bg-background/20 rounded-md border border-border flex items-center gap-2"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="text-muted-foreground flex-1">
|
||||
<span className="flex-1 text-muted-foreground">
|
||||
https://demo.openpanel.dev
|
||||
</span>
|
||||
<ArrowRightIcon className="size-4 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<ArrowRightIcon className="size-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</a>
|
||||
</div>
|
||||
<iframe
|
||||
src={'https://demo.openpanel.dev/demo/shoey?range=lastHour'}
|
||||
className="w-full h-full"
|
||||
title="Live preview"
|
||||
className="h-full w-full"
|
||||
scrolling="no"
|
||||
src={'https://demo.openpanel.dev/demo/shoey?range=lastHour'}
|
||||
title="Live preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,13 +171,13 @@ export function HeroContainer({
|
||||
<HeroImage />
|
||||
</div>
|
||||
<div
|
||||
className="container relative col sm:row py-44 max-sm:pt-32"
|
||||
className="col sm:row container relative py-44 max-sm:pt-32"
|
||||
data-children
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{divider && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 border-t border-border rounded-t-[3rem] md:rounded-t-[6rem] bg-background shadow-[0_0_100px_var(--background)]" />
|
||||
<div className="absolute right-0 bottom-0 left-0 h-20 rounded-t-[3rem] border-border border-t bg-background shadow-[0_0_100px_var(--background)] md:rounded-t-[6rem]" />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import NumberFlow from '@number-flow/react';
|
||||
import { PRICING } from '@openpanel/payments/prices';
|
||||
import { CheckIcon, ServerIcon, StarIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import NumberFlow from '@number-flow/react';
|
||||
import { PRICING } from '@openpanel/payments/prices';
|
||||
|
||||
import { CheckIcon, StarIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { formatEventsCount } from '@/lib/utils';
|
||||
import { cn, formatEventsCount } from '@/lib/utils';
|
||||
|
||||
const features = [
|
||||
'Unlimited websites or apps',
|
||||
@@ -28,51 +26,56 @@ export function Pricing() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<div className="col md:row gap-16">
|
||||
<div className="w-full md:w-1/3 min-w-sm col gap-4 border rounded-3xl p-6 bg-linear-to-b from-card to-background">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="col w-full min-w-sm gap-4 rounded-3xl border bg-linear-to-b from-card to-background p-6 md:w-1/3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Choose how many events you'll track this month
|
||||
</p>
|
||||
<div className="row flex-wrap gap-2">
|
||||
{PRICING.map((tier, index) => (
|
||||
<Button
|
||||
className={cn('relative h-8 rounded-full border px-4')}
|
||||
key={tier.price}
|
||||
variant={selectedIndex === index ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn('h-8 rounded-full relative px-4 border')}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
size="sm"
|
||||
variant={selectedIndex === index ? 'default' : 'outline'}
|
||||
>
|
||||
{tier.popular && <StarIcon className="size-4" />}
|
||||
{formatEventsCount(tier.events)}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant={selectedIndex === -1 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn('h-8 rounded-full relative px-4 border')}
|
||||
className={cn('relative h-8 rounded-full border px-4')}
|
||||
onClick={() => setSelectedIndex(-1)}
|
||||
size="sm"
|
||||
variant={selectedIndex === -1 ? 'default' : 'outline'}
|
||||
>
|
||||
Custom
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col items-baseline mt-8 md:mt-auto w-full">
|
||||
<div className="col mt-8 w-full items-baseline md:mt-auto">
|
||||
{selected ? (
|
||||
<>
|
||||
<NumberFlow
|
||||
className="text-5xl font-bold"
|
||||
value={selected.price}
|
||||
format={{
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}}
|
||||
locales={'en-US'}
|
||||
/>
|
||||
<div className="row justify-between w-full">
|
||||
<span className="text-muted-foreground/80 text-sm -mt-2">
|
||||
<div className="row items-end gap-3">
|
||||
<NumberFlow
|
||||
className="font-bold text-5xl"
|
||||
format={{
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}}
|
||||
locales={'en-US'}
|
||||
value={selected.price}
|
||||
/>
|
||||
<span className="mb-2 rounded-full bg-primary/10 px-2.5 py-0.5 font-medium text-primary text-xs">
|
||||
30-day free trial
|
||||
</span>
|
||||
</div>
|
||||
<div className="row w-full justify-between">
|
||||
<span className="-mt-2 text-muted-foreground/80 text-sm">
|
||||
Per month
|
||||
</span>
|
||||
<span className="text-muted-foreground/80 text-sm -mt-2">
|
||||
<span className="-mt-2 text-muted-foreground/80 text-sm">
|
||||
+ VAT if applicable
|
||||
</span>
|
||||
</div>
|
||||
@@ -89,18 +92,18 @@ export function Pricing() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col gap-8 justify-center flex-1 shrink-0">
|
||||
<div className="col flex-1 shrink-0 justify-center gap-8">
|
||||
<div className="col gap-4">
|
||||
<SectionHeader
|
||||
title="Simple, transparent pricing"
|
||||
description="Pay only for what you use. Choose your event volume - everything else is unlimited. No surprises, no hidden fees."
|
||||
title="Simple, transparent pricing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className="col gap-2">
|
||||
{features.map((feature) => (
|
||||
<li key={feature} className="row gap-2 items-start text-sm">
|
||||
<CheckIcon className="size-4 shrink-0 mt-0.5" />
|
||||
<li className="row items-start gap-2 text-sm" key={feature}>
|
||||
<CheckIcon className="mt-0.5 size-4 shrink-0" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -108,9 +111,16 @@ export function Pricing() {
|
||||
|
||||
<GetStartedButton className="w-fit" />
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All features are included upfront - no hidden costs. You choose how
|
||||
many events to track each month.
|
||||
<p className="row items-center gap-2 text-muted-foreground text-sm">
|
||||
<ServerIcon className="size-4 shrink-0" />
|
||||
Prefer to self-host?{' '}
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
href="/docs/self-hosting/self-hosting"
|
||||
>
|
||||
Deploy for free
|
||||
</Link>{' '}
|
||||
with unlimited events.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { getAllCompareSlugs, getCompareData } from '@/lib/compare';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { getCompareData } from '@/lib/compare';
|
||||
import { getFeatureData } from '@/lib/features';
|
||||
import { url as baseUrl } from '@/lib/layout.shared';
|
||||
import { articleSource, guideSource, pageSource, source } from '@/lib/source';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
// Truncate text helper
|
||||
function truncateText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return `${text.substring(0, maxLength).trim()}...`;
|
||||
}
|
||||
|
||||
async function getOgData(
|
||||
segments: string[],
|
||||
segments: string[]
|
||||
): Promise<{ title: string; description?: string }> {
|
||||
switch (segments[0]) {
|
||||
case 'default':
|
||||
@@ -34,6 +36,13 @@ async function getOgData(
|
||||
"Get free web and product analytics for your open source project. Track up to 2.5M events/month. Apply to OpenPanel's open source program today.",
|
||||
};
|
||||
}
|
||||
case 'open-source-analytics': {
|
||||
return {
|
||||
title: 'Open Source Analytics for Web and Product Teams',
|
||||
description:
|
||||
'OpenPanel is an open source analytics platform that combines web analytics and product analytics in one privacy-first tool. Track pageviews, events, funnels, retention, and user journeys — all without cookies.',
|
||||
};
|
||||
}
|
||||
case 'pricing': {
|
||||
return {
|
||||
title: 'Pricing',
|
||||
@@ -188,7 +197,7 @@ async function getOgData(
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ og: string[] }> },
|
||||
{ params }: { params: Promise<{ og: string[] }> }
|
||||
) {
|
||||
try {
|
||||
const { og } = await params;
|
||||
@@ -208,10 +217,10 @@ export async function GET(
|
||||
// Fetch Geist font files from CDN (cache fonts for better performance)
|
||||
const [geistRegular, geistBold] = await Promise.all([
|
||||
fetch(
|
||||
'https://cdn.jsdelivr.net/npm/geist@1.5.1/dist/fonts/geist-sans/Geist-Regular.ttf',
|
||||
'https://cdn.jsdelivr.net/npm/geist@1.5.1/dist/fonts/geist-sans/Geist-Regular.ttf'
|
||||
).then((res) => res.arrayBuffer()),
|
||||
fetch(
|
||||
'https://cdn.jsdelivr.net/npm/geist@1.5.1/dist/fonts/geist-sans/Geist-Bold.ttf',
|
||||
'https://cdn.jsdelivr.net/npm/geist@1.5.1/dist/fonts/geist-sans/Geist-Bold.ttf'
|
||||
).then((res) => res.arrayBuffer()),
|
||||
]);
|
||||
|
||||
@@ -288,7 +297,7 @@ export async function GET(
|
||||
weight: 700,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to generate OG image: ${e.message}`);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import {
|
||||
articleSource,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
pageSource,
|
||||
source,
|
||||
} from '@/lib/source';
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const articles = await articleSource.getPages();
|
||||
@@ -45,6 +45,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.5,
|
||||
},
|
||||
{
|
||||
url: url('/open-source-analytics'),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: url('/features'),
|
||||
lastModified: new Date(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface FeatureCardProps {
|
||||
link?: {
|
||||
@@ -21,12 +21,17 @@ interface FeatureCardContainerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FeatureCardBackground = () => (
|
||||
export const FeatureCardBackground = ({
|
||||
interactive = true,
|
||||
}: {
|
||||
interactive?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 bg-linear-to-br opacity-0 blur-2xl transition-opacity duration-300 group-hover:opacity-100',
|
||||
'dark:from-blue-500/10 dark:via-transparent dark:to-emerald-500/5',
|
||||
'dark:from-blue-500/20 dark:via-transparent dark:to-emerald-500/10',
|
||||
'light:from-blue-800/20 light:via-transparent light:to-emerald-900/10',
|
||||
interactive === false && 'opacity-100'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -38,8 +43,8 @@ export function FeatureCardContainer({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'col gap-8 p-6 rounded-3xl border bg-background group relative overflow-hidden',
|
||||
className,
|
||||
'col group relative gap-8 overflow-hidden rounded-3xl border bg-background p-6',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FeatureCardBackground />
|
||||
@@ -62,13 +67,13 @@ export function FeatureCard({
|
||||
<FeatureCardContainer className={className}>
|
||||
{illustration}
|
||||
<div className="col gap-2" data-content>
|
||||
<h3 className="text-xl font-semibold">{title}</h3>
|
||||
<h3 className="font-semibold text-xl">{title}</h3>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
{children}
|
||||
{link && (
|
||||
<Link
|
||||
className="mx-6 text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
className="mx-6 text-muted-foreground text-sm transition-colors hover:text-primary"
|
||||
href={link.href}
|
||||
>
|
||||
{link.children}
|
||||
@@ -82,13 +87,13 @@ export function FeatureCard({
|
||||
<FeatureCardContainer className={className}>
|
||||
{Icon && <Icon className="size-6" />}
|
||||
<div className="col gap-2">
|
||||
<h3 className="text-lg font-semibold">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
<h3 className="font-semibold text-lg">{title}</h3>
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
</div>
|
||||
{children}
|
||||
{link && (
|
||||
<Link
|
||||
className="text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
className="text-muted-foreground text-sm transition-colors hover:text-primary"
|
||||
href={link.href}
|
||||
>
|
||||
{link.children}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { TOOLS } from '@/app/tools/tools';
|
||||
import { articleSource, compareSource, featureSource } from '@/lib/source';
|
||||
import { MailIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Logo } from './logo';
|
||||
import { TOOLS } from '@/app/tools/tools';
|
||||
import { articleSource, compareSource, featureSource } from '@/lib/source';
|
||||
export async function Footer() {
|
||||
const articles = (await articleSource.getPages()).sort(
|
||||
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
|
||||
(a, b) => b.data.date.getTime() - a.data.date.getTime()
|
||||
);
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<footer className="text-sm relative overflow-hidden pt-32">
|
||||
<div className="absolute -bottom-20 md:-bottom-32 left-0 right-0 center-center opacity-5 pointer-events-none">
|
||||
<footer className="relative overflow-hidden pt-32 text-sm">
|
||||
<div className="center-center pointer-events-none absolute right-0 -bottom-20 left-0 opacity-5 md:-bottom-32">
|
||||
<div className="absolute inset-0 bg-linear-to-b from-background to-transparent" />
|
||||
<Logo className="w-[900px] shrink-0" />
|
||||
</div>
|
||||
<div className="container grid grid-cols-1 md:grid-cols-4 gap-12 md:gap-8 relative">
|
||||
<div className="container relative grid grid-cols-1 gap-12 md:grid-cols-4 md:gap-8">
|
||||
<div className="col gap-3">
|
||||
<h3 className="font-medium">Useful links</h3>
|
||||
<Links
|
||||
@@ -28,6 +28,10 @@ export async function Footer() {
|
||||
title: 'Free analytics for open source projects',
|
||||
url: '/open-source',
|
||||
},
|
||||
{
|
||||
title: 'Open source analytics',
|
||||
url: '/open-source-analytics',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="h-5" />
|
||||
@@ -86,8 +90,8 @@ export async function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col text-muted-foreground border-t pt-8 mt-16 gap-8 relative bg-background/70 pb-32">
|
||||
<div className="container col md:row justify-between gap-8">
|
||||
<div className="col relative mt-16 gap-8 border-t bg-background/70 pt-8 pb-32 text-muted-foreground">
|
||||
<div className="col md:row container justify-between gap-8">
|
||||
<div>
|
||||
<a
|
||||
href="https://openpanel.dev"
|
||||
@@ -100,21 +104,21 @@ export async function Footer() {
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src="https://dashboard.openpanel.dev/widget/badge?shareId=ancygl&color=%230B0B0B"
|
||||
height="48"
|
||||
width="100%"
|
||||
src="https://dashboard.openpanel.dev/widget/badge?shareId=ancygl&color=%230B0B0B"
|
||||
style={{
|
||||
border: 'none',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
title="OpenPanel Analytics Badge"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<Social />
|
||||
</div>
|
||||
<div className="container flex flex-col-reverse md:row justify-between gap-8">
|
||||
<div className="md:row container flex flex-col-reverse justify-between gap-8">
|
||||
<div>Copyright © {year} OpenPanel. All rights reserved.</div>
|
||||
<div className="col lg:row gap-2 md:gap-4">
|
||||
<Link href="/sitemap.xml">Sitemap</Link>
|
||||
@@ -131,12 +135,12 @@ export async function Footer() {
|
||||
|
||||
function Links({ data }: { data: { title: string; url: string }[] }) {
|
||||
return (
|
||||
<ul className="gap-2 col text-muted-foreground">
|
||||
<ul className="col gap-2 text-muted-foreground">
|
||||
{data.map((item) => (
|
||||
<li key={item.url} className="truncate">
|
||||
<li className="truncate" key={item.url}>
|
||||
<Link
|
||||
className="transition-colors hover:text-foreground"
|
||||
href={item.url}
|
||||
className="hover:text-foreground transition-colors"
|
||||
title={item.title}
|
||||
>
|
||||
{item.title}
|
||||
@@ -149,68 +153,68 @@ function Links({ data }: { data: { title: string; url: string }[] }) {
|
||||
|
||||
function Social() {
|
||||
return (
|
||||
<div className="md:items-end col gap-4">
|
||||
<div className="[&_svg]:size-6 row gap-4">
|
||||
<div className="col gap-4 md:items-end">
|
||||
<div className="row gap-4 [&_svg]:size-6">
|
||||
<Link
|
||||
title="Go to GitHub"
|
||||
href="https://github.com/Openpanel-dev/openpanel"
|
||||
rel="noreferrer noopener nofollow"
|
||||
title="Go to GitHub"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="fill-current"
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
title="Go to X"
|
||||
href="https://x.com/openpaneldev"
|
||||
rel="noreferrer noopener nofollow"
|
||||
title="Go to X"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="fill-current"
|
||||
>
|
||||
<title>X</title>
|
||||
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H3.298Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
title="Join Discord"
|
||||
href="https://go.openpanel.dev/discord"
|
||||
rel="noreferrer noopener nofollow"
|
||||
title="Join Discord"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="fill-current"
|
||||
>
|
||||
<title>Discord</title>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
title="Send an email"
|
||||
href="mailto:hello@openpanel.dev"
|
||||
rel="noreferrer noopener nofollow"
|
||||
title="Send an email"
|
||||
>
|
||||
<MailIcon className="size-6" />
|
||||
</Link>
|
||||
<a
|
||||
target="_blank"
|
||||
className="row items-center gap-2 rounded-full border px-2 py-1 max-md:ml-auto max-md:self-start"
|
||||
href="https://status.openpanel.dev"
|
||||
className="row gap-2 items-center border rounded-full px-2 py-1 max-md:self-start max-md:ml-auto"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Operational</span>
|
||||
<div className="size-2 bg-emerald-500 rounded-full" />
|
||||
<div className="size-2 rounded-full bg-emerald-500" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from './ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function GetStartedButton({
|
||||
text,
|
||||
@@ -13,10 +13,10 @@ export function GetStartedButton({
|
||||
href?: string;
|
||||
}) {
|
||||
return (
|
||||
<Button size="lg" asChild className={cn('group', className)}>
|
||||
<Button asChild className={cn('group', className)} size="lg">
|
||||
<Link href={href}>
|
||||
{text ?? 'Get started now'}
|
||||
<ChevronRightIcon className="size-4 group-hover:translate-x-1 transition-transform group-hover:scale-125" />
|
||||
{text ?? 'Start free trial'}
|
||||
<ChevronRightIcon className="size-4 transition-transform group-hover:translate-x-1 group-hover:scale-125" />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -21,10 +21,10 @@ const LINKS = [
|
||||
text: 'Pricing',
|
||||
url: '/pricing',
|
||||
},
|
||||
{
|
||||
text: 'Supporter',
|
||||
url: '/supporter',
|
||||
},
|
||||
// {
|
||||
// text: 'Supporter',
|
||||
// url: '/supporter',
|
||||
// },
|
||||
{
|
||||
text: 'Docs',
|
||||
url: '/docs',
|
||||
|
||||
22
apps/public/src/components/testimonial.tsx
Normal file
22
apps/public/src/components/testimonial.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { QuoteIcon } from 'lucide-react';
|
||||
import { FeatureCardBackground } from '@/components/feature-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TestimonialProps {
|
||||
quote: string;
|
||||
author: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Testimonial({ quote, author, className }: TestimonialProps) {
|
||||
return (
|
||||
<figure className={cn('group relative', className)}>
|
||||
<FeatureCardBackground interactive={false} />
|
||||
<QuoteIcon className="group-hover:-translate-1 mb-2 size-8 stroke-1 text-muted-foreground/50 transition-all group-hover:-rotate-6 group-hover:scale-105 group-hover:text-foreground" />
|
||||
<blockquote className="text-2xl">{quote}</blockquote>
|
||||
<figcaption className="mt-2 text-muted-foreground text-sm">
|
||||
— {author}
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
HeadContent,
|
||||
Scripts,
|
||||
createRootRouteWithContext,
|
||||
} from '@tanstack/react-router';
|
||||
|
||||
import 'flag-icons/css/flag-icons.min.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import appCss from '../styles.css?url';
|
||||
|
||||
import type { AppRouter } from '@openpanel/trpc';
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
||||
import appCss from '../styles.css?url';
|
||||
import type { ConfigResonse } from './api/config';
|
||||
import { FullPageErrorState } from '@/components/full-page-error-state';
|
||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||
import { Providers } from '@/components/providers';
|
||||
@@ -20,27 +21,21 @@ import { LinkButton } from '@/components/ui/button';
|
||||
import { getCookiesFn } from '@/hooks/use-cookie-store';
|
||||
import { useSessionExtension } from '@/hooks/use-session-extension';
|
||||
import { op } from '@/utils/op';
|
||||
import type { AppRouter } from '@openpanel/trpc';
|
||||
import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
||||
|
||||
if (import.meta.env.VITE_OP_CLIENT_ID) {
|
||||
op.init();
|
||||
}
|
||||
|
||||
interface MyRouterContext {
|
||||
interface MyRouterContext extends ConfigResonse {
|
||||
queryClient: QueryClient;
|
||||
trpc: TRPCOptionsProxy<AppRouter>;
|
||||
apiUrl: string;
|
||||
dashboardUrl: string;
|
||||
isSelfHosted: boolean;
|
||||
isMaintenance: boolean;
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
beforeLoad: async ({ context }) => {
|
||||
const [session, cookies] = await Promise.all([
|
||||
context.queryClient.ensureQueryData(
|
||||
context.trpc.auth.session.queryOptions(),
|
||||
context.trpc.auth.session.queryOptions()
|
||||
),
|
||||
getCookiesFn().catch(() => ({}) as Record<string, string>),
|
||||
]);
|
||||
@@ -68,8 +63,8 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
shellComponent: RootDocument,
|
||||
errorComponent: ({ error }) => (
|
||||
<FullPageErrorState
|
||||
title={'Something went wrong'}
|
||||
description={error.message}
|
||||
title={'Something went wrong'}
|
||||
>
|
||||
<LinkButton href="/">Go back to home</LinkButton>
|
||||
</FullPageErrorState>
|
||||
@@ -85,7 +80,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="grainy min-h-screen bg-def-100 font-sans text-base antialiased leading-normal">
|
||||
<body className="grainy min-h-screen bg-def-100 font-sans text-base leading-normal antialiased">
|
||||
<Providers>{children}</Providers>
|
||||
<ThemeScriptOnce />
|
||||
<Scripts />
|
||||
@@ -100,150 +95,150 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
}}
|
||||
/>
|
||||
<div className="hidden">
|
||||
<div className="text-chart-0 bg-chart-0" />
|
||||
<div className="text-chart-1 bg-chart-1" />
|
||||
<div className="text-chart-2 bg-chart-2" />
|
||||
<div className="text-chart-3 bg-chart-3" />
|
||||
<div className="text-chart-4 bg-chart-4" />
|
||||
<div className="text-chart-5 bg-chart-5" />
|
||||
<div className="text-chart-6 bg-chart-6" />
|
||||
<div className="text-chart-7 bg-chart-7" />
|
||||
<div className="text-chart-8 bg-chart-8" />
|
||||
<div className="text-chart-9 bg-chart-9" />
|
||||
<div className="text-chart-10 bg-chart-10" />
|
||||
<div className="text-chart-11 bg-chart-11" />
|
||||
<div className="text-rose-50 bg-rose-50 hover:bg-rose-50 border-rose-50 dark:bg-rose-50 dark:hover:bg-rose-50" />
|
||||
<div className="text-rose-100 bg-rose-100 hover:bg-rose-100 border-rose-100 dark:bg-rose-100 dark:hover:bg-rose-100" />
|
||||
<div className="text-rose-200 bg-rose-200 hover:bg-rose-200 border-rose-200 dark:bg-rose-200 dark:hover:bg-rose-200" />
|
||||
<div className="text-rose-700 bg-rose-700 hover:bg-rose-700 border-rose-700 dark:bg-rose-700 dark:hover:bg-rose-700" />
|
||||
<div className="text-rose-800 bg-rose-800 hover:bg-rose-800 border-rose-800 dark:bg-rose-800 dark:hover:bg-rose-800" />
|
||||
<div className="text-rose-900 bg-rose-900 hover:bg-rose-900 border-rose-900 dark:bg-rose-900 dark:hover:bg-rose-900" />
|
||||
<div className="text-pink-50 bg-pink-50 hover:bg-pink-50 border-pink-50 dark:bg-pink-50 dark:hover:bg-pink-50" />
|
||||
<div className="text-pink-100 bg-pink-100 hover:bg-pink-100 border-pink-100 dark:bg-pink-100 dark:hover:bg-pink-100" />
|
||||
<div className="text-pink-200 bg-pink-200 hover:bg-pink-200 border-pink-200 dark:bg-pink-200 dark:hover:bg-pink-200" />
|
||||
<div className="text-pink-700 bg-pink-700 hover:bg-pink-700 border-pink-700 dark:bg-pink-700 dark:hover:bg-pink-700" />
|
||||
<div className="text-pink-800 bg-pink-800 hover:bg-pink-800 border-pink-800 dark:bg-pink-800 dark:hover:bg-pink-800" />
|
||||
<div className="text-pink-900 bg-pink-900 hover:bg-pink-900 border-pink-900 dark:bg-pink-900 dark:hover:bg-pink-900" />
|
||||
<div className="text-fuchsia-50 bg-fuchsia-50 hover:bg-fuchsia-50 border-fuchsia-50 dark:bg-fuchsia-50 dark:hover:bg-fuchsia-50" />
|
||||
<div className="text-fuchsia-100 bg-fuchsia-100 hover:bg-fuchsia-100 border-fuchsia-100 dark:bg-fuchsia-100 dark:hover:bg-fuchsia-100" />
|
||||
<div className="text-fuchsia-200 bg-fuchsia-200 hover:bg-fuchsia-200 border-fuchsia-200 dark:bg-fuchsia-200 dark:hover:bg-fuchsia-200" />
|
||||
<div className="text-fuchsia-700 bg-fuchsia-700 hover:bg-fuchsia-700 border-fuchsia-700 dark:bg-fuchsia-700 dark:hover:bg-fuchsia-700" />
|
||||
<div className="text-fuchsia-800 bg-fuchsia-800 hover:bg-fuchsia-800 border-fuchsia-800 dark:bg-fuchsia-800 dark:hover:bg-fuchsia-800" />
|
||||
<div className="text-fuchsia-900 bg-fuchsia-900 hover:bg-fuchsia-900 border-fuchsia-900 dark:bg-fuchsia-900 dark:hover:bg-fuchsia-900" />
|
||||
<div className="text-purple-50 bg-purple-50 hover:bg-purple-50 border-purple-50 dark:bg-purple-50 dark:hover:bg-purple-50" />
|
||||
<div className="text-purple-100 bg-purple-100 hover:bg-purple-100 border-purple-100 dark:bg-purple-100 dark:hover:bg-purple-100" />
|
||||
<div className="text-purple-200 bg-purple-200 hover:bg-purple-200 border-purple-200 dark:bg-purple-200 dark:hover:bg-purple-200" />
|
||||
<div className="text-purple-700 bg-purple-700 hover:bg-purple-700 border-purple-700 dark:bg-purple-700 dark:hover:bg-purple-700" />
|
||||
<div className="text-purple-800 bg-purple-800 hover:bg-purple-800 border-purple-800 dark:bg-purple-800 dark:hover:bg-purple-800" />
|
||||
<div className="text-purple-900 bg-purple-900 hover:bg-purple-900 border-purple-900 dark:bg-purple-900 dark:hover:bg-purple-900" />
|
||||
<div className="text-violet-50 bg-violet-50 hover:bg-violet-50 border-violet-50 dark:bg-violet-50 dark:hover:bg-violet-50" />
|
||||
<div className="text-violet-100 bg-violet-100 hover:bg-violet-100 border-violet-100 dark:bg-violet-100 dark:hover:bg-violet-100" />
|
||||
<div className="text-violet-200 bg-violet-200 hover:bg-violet-200 border-violet-200 dark:bg-violet-200 dark:hover:bg-violet-200" />
|
||||
<div className="text-violet-700 bg-violet-700 hover:bg-violet-700 border-violet-700 dark:bg-violet-700 dark:hover:bg-violet-700" />
|
||||
<div className="text-violet-800 bg-violet-800 hover:bg-violet-800 border-violet-800 dark:bg-violet-800 dark:hover:bg-violet-800" />
|
||||
<div className="text-violet-900 bg-violet-900 hover:bg-violet-900 border-violet-900 dark:bg-violet-900 dark:hover:bg-violet-900" />
|
||||
<div className="text-indigo-50 bg-indigo-50 hover:bg-indigo-50 border-indigo-50 dark:bg-indigo-50 dark:hover:bg-indigo-50" />
|
||||
<div className="text-indigo-100 bg-indigo-100 hover:bg-indigo-100 border-indigo-100 dark:bg-indigo-100 dark:hover:bg-indigo-100" />
|
||||
<div className="text-indigo-200 bg-indigo-200 hover:bg-indigo-200 border-indigo-200 dark:bg-indigo-200 dark:hover:bg-indigo-200" />
|
||||
<div className="text-indigo-700 bg-indigo-700 hover:bg-indigo-700 border-indigo-700 dark:bg-indigo-700 dark:hover:bg-indigo-700" />
|
||||
<div className="text-indigo-800 bg-indigo-800 hover:bg-indigo-800 border-indigo-800 dark:bg-indigo-800 dark:hover:bg-indigo-800" />
|
||||
<div className="text-indigo-900 bg-indigo-900 hover:bg-indigo-900 border-indigo-900 dark:bg-indigo-900 dark:hover:bg-indigo-900" />
|
||||
<div className="text-blue-50 bg-blue-50 hover:bg-blue-50 border-blue-50 dark:bg-blue-50 dark:hover:bg-blue-50" />
|
||||
<div className="text-blue-100 bg-blue-100 hover:bg-blue-100 border-blue-100 dark:bg-blue-100 dark:hover:bg-blue-100" />
|
||||
<div className="text-blue-200 bg-blue-200 hover:bg-blue-200 border-blue-200 dark:bg-blue-200 dark:hover:bg-blue-200" />
|
||||
<div className="text-blue-700 bg-blue-700 hover:bg-blue-700 border-blue-700 dark:bg-blue-700 dark:hover:bg-blue-700" />
|
||||
<div className="text-blue-800 bg-blue-800 hover:bg-blue-800 border-blue-800 dark:bg-blue-800 dark:hover:bg-blue-800" />
|
||||
<div className="text-blue-900 bg-blue-900 hover:bg-blue-900 border-blue-900 dark:bg-blue-900 dark:hover:bg-blue-900" />
|
||||
<div className="text-sky-50 bg-sky-50 hover:bg-sky-50 border-sky-50 dark:bg-sky-50 dark:hover:bg-sky-50" />
|
||||
<div className="text-sky-100 bg-sky-100 hover:bg-sky-100 border-sky-100 dark:bg-sky-100 dark:hover:bg-sky-100" />
|
||||
<div className="text-sky-200 bg-sky-200 hover:bg-sky-200 border-sky-200 dark:bg-sky-200 dark:hover:bg-sky-200" />
|
||||
<div className="text-sky-700 bg-sky-700 hover:bg-sky-700 border-sky-700 dark:bg-sky-700 dark:hover:bg-sky-700" />
|
||||
<div className="text-sky-800 bg-sky-800 hover:bg-sky-800 border-sky-800 dark:bg-sky-800 dark:hover:bg-sky-800" />
|
||||
<div className="text-sky-900 bg-sky-900 hover:bg-sky-900 border-sky-900 dark:bg-sky-900 dark:hover:bg-sky-900" />
|
||||
<div className="text-cyan-50 bg-cyan-50 hover:bg-cyan-50 border-cyan-50 dark:bg-cyan-50 dark:hover:bg-cyan-50" />
|
||||
<div className="text-cyan-100 bg-cyan-100 hover:bg-cyan-100 border-cyan-100 dark:bg-cyan-100 dark:hover:bg-cyan-100" />
|
||||
<div className="text-cyan-200 bg-cyan-200 hover:bg-cyan-200 border-cyan-200 dark:bg-cyan-200 dark:hover:bg-cyan-200" />
|
||||
<div className="text-cyan-700 bg-cyan-700 hover:bg-cyan-700 border-cyan-700 dark:bg-cyan-700 dark:hover:bg-cyan-700" />
|
||||
<div className="text-cyan-800 bg-cyan-800 hover:bg-cyan-800 border-cyan-800 dark:bg-cyan-800 dark:hover:bg-cyan-800" />
|
||||
<div className="text-cyan-900 bg-cyan-900 hover:bg-cyan-900 border-cyan-900 dark:bg-cyan-900 dark:hover:bg-cyan-900" />
|
||||
<div className="text-teal-50 bg-teal-50 hover:bg-teal-50 border-teal-50 dark:bg-teal-50 dark:hover:bg-teal-50" />
|
||||
<div className="text-teal-100 bg-teal-100 hover:bg-teal-100 border-teal-100 dark:bg-teal-100 dark:hover:bg-teal-100" />
|
||||
<div className="text-teal-200 bg-teal-200 hover:bg-teal-200 border-teal-200 dark:bg-teal-200 dark:hover:bg-teal-200" />
|
||||
<div className="text-teal-700 bg-teal-700 hover:bg-teal-700 border-teal-700 dark:bg-teal-700 dark:hover:bg-teal-700" />
|
||||
<div className="text-teal-800 bg-teal-800 hover:bg-teal-800 border-teal-800 dark:bg-teal-800 dark:hover:bg-teal-800" />
|
||||
<div className="text-teal-900 bg-teal-900 hover:bg-teal-900 border-teal-900 dark:bg-teal-900 dark:hover:bg-teal-900" />
|
||||
<div className="text-emerald-50 bg-emerald-50 hover:bg-emerald-50 border-emerald-50 dark:bg-emerald-50 dark:hover:bg-emerald-50" />
|
||||
<div className="text-emerald-100 bg-emerald-100 hover:bg-emerald-100 border-emerald-100 dark:bg-emerald-100 dark:hover:bg-emerald-100" />
|
||||
<div className="text-emerald-200 bg-emerald-200 hover:bg-emerald-200 border-emerald-200 dark:bg-emerald-200 dark:hover:bg-emerald-200" />
|
||||
<div className="text-emerald-700 bg-emerald-700 hover:bg-emerald-700 border-emerald-700 dark:bg-emerald-700 dark:hover:bg-emerald-700" />
|
||||
<div className="text-emerald-800 bg-emerald-800 hover:bg-emerald-800 border-emerald-800 dark:bg-emerald-800 dark:hover:bg-emerald-800" />
|
||||
<div className="text-emerald-900 bg-emerald-900 hover:bg-emerald-900 border-emerald-900 dark:bg-emerald-900 dark:hover:bg-emerald-900" />
|
||||
<div className="text-green-50 bg-green-50 hover:bg-green-50 border-green-50 dark:bg-green-50 dark:hover:bg-green-50" />
|
||||
<div className="text-green-100 bg-green-100 hover:bg-green-100 border-green-100 dark:bg-green-100 dark:hover:bg-green-100" />
|
||||
<div className="text-green-200 bg-green-200 hover:bg-green-200 border-green-200 dark:bg-green-200 dark:hover:bg-green-200" />
|
||||
<div className="text-green-700 bg-green-700 hover:bg-green-700 border-green-700 dark:bg-green-700 dark:hover:bg-green-700" />
|
||||
<div className="text-green-800 bg-green-800 hover:bg-green-800 border-green-800 dark:bg-green-800 dark:hover:bg-green-800" />
|
||||
<div className="text-green-900 bg-green-900 hover:bg-green-900 border-green-900 dark:bg-green-900 dark:hover:bg-green-900" />
|
||||
<div className="text-lime-50 bg-lime-50 hover:bg-lime-50 border-lime-50 dark:bg-lime-50 dark:hover:bg-lime-50" />
|
||||
<div className="text-lime-100 bg-lime-100 hover:bg-lime-100 border-lime-100 dark:bg-lime-100 dark:hover:bg-lime-100" />
|
||||
<div className="text-lime-200 bg-lime-200 hover:bg-lime-200 border-lime-200 dark:bg-lime-200 dark:hover:bg-lime-200" />
|
||||
<div className="text-lime-700 bg-lime-700 hover:bg-lime-700 border-lime-700 dark:bg-lime-700 dark:hover:bg-lime-700" />
|
||||
<div className="text-lime-800 bg-lime-800 hover:bg-lime-800 border-lime-800 dark:bg-lime-800 dark:hover:bg-lime-800" />
|
||||
<div className="text-lime-900 bg-lime-900 hover:bg-lime-900 border-lime-900 dark:bg-lime-900 dark:hover:bg-lime-900" />
|
||||
<div className="text-yellow-50 bg-yellow-50 hover:bg-yellow-50 border-yellow-50 dark:bg-yellow-50 dark:hover:bg-yellow-50" />
|
||||
<div className="text-yellow-100 bg-yellow-100 hover:bg-yellow-100 border-yellow-100 dark:bg-yellow-100 dark:hover:bg-yellow-100" />
|
||||
<div className="text-yellow-200 bg-yellow-200 hover:bg-yellow-200 border-yellow-200 dark:bg-yellow-200 dark:hover:bg-yellow-200" />
|
||||
<div className="text-yellow-700 bg-yellow-700 hover:bg-yellow-700 border-yellow-700 dark:bg-yellow-700 dark:hover:bg-yellow-700" />
|
||||
<div className="text-yellow-800 bg-yellow-800 hover:bg-yellow-800 border-yellow-800 dark:bg-yellow-800 dark:hover:bg-yellow-800" />
|
||||
<div className="text-yellow-900 bg-yellow-900 hover:bg-yellow-900 border-yellow-900 dark:bg-yellow-900 dark:hover:bg-yellow-900" />
|
||||
<div className="text-amber-50 bg-amber-50 hover:bg-amber-50 border-amber-50 dark:bg-amber-50 dark:hover:bg-amber-50" />
|
||||
<div className="text-amber-100 bg-amber-100 hover:bg-amber-100 border-amber-100 dark:bg-amber-100 dark:hover:bg-amber-100" />
|
||||
<div className="text-amber-200 bg-amber-200 hover:bg-amber-200 border-amber-200 dark:bg-amber-200 dark:hover:bg-amber-200" />
|
||||
<div className="text-amber-700 bg-amber-700 hover:bg-amber-700 border-amber-700 dark:bg-amber-700 dark:hover:bg-amber-700" />
|
||||
<div className="text-amber-800 bg-amber-800 hover:bg-amber-800 border-amber-800 dark:bg-amber-800 dark:hover:bg-amber-800" />
|
||||
<div className="text-amber-900 bg-amber-900 hover:bg-amber-900 border-amber-900 dark:bg-amber-900 dark:hover:bg-amber-900" />
|
||||
<div className="text-orange-50 bg-orange-50 hover:bg-orange-50 border-orange-50 dark:bg-orange-50 dark:hover:bg-orange-50" />
|
||||
<div className="text-orange-100 bg-orange-100 hover:bg-orange-100 border-orange-100 dark:bg-orange-100 dark:hover:bg-orange-100" />
|
||||
<div className="text-orange-200 bg-orange-200 hover:bg-orange-200 border-orange-200 dark:bg-orange-200 dark:hover:bg-orange-200" />
|
||||
<div className="text-orange-700 bg-orange-700 hover:bg-orange-700 border-orange-700 dark:bg-orange-700 dark:hover:bg-orange-700" />
|
||||
<div className="text-orange-800 bg-orange-800 hover:bg-orange-800 border-orange-800 dark:bg-orange-800 dark:hover:bg-orange-800" />
|
||||
<div className="text-orange-900 bg-orange-900 hover:bg-orange-900 border-orange-900 dark:bg-orange-900 dark:hover:bg-orange-900" />
|
||||
<div className="text-red-50 bg-red-50 hover:bg-red-50 border-red-50 dark:bg-red-50 dark:hover:bg-red-50" />
|
||||
<div className="text-red-100 bg-red-100 hover:bg-red-100 border-red-100 dark:bg-red-100 dark:hover:bg-red-100" />
|
||||
<div className="text-red-200 bg-red-200 hover:bg-red-200 border-red-200 dark:bg-red-200 dark:hover:bg-red-200" />
|
||||
<div className="text-red-700 bg-red-700 hover:bg-red-700 border-red-700 dark:bg-red-700 dark:hover:bg-red-700" />
|
||||
<div className="text-red-800 bg-red-800 hover:bg-red-800 border-red-800 dark:bg-red-800 dark:hover:bg-red-800" />
|
||||
<div className="text-red-900 bg-red-900 hover:bg-red-900 border-red-900 dark:bg-red-900 dark:hover:bg-red-900" />
|
||||
<div className="text-stone-50 bg-stone-50 hover:bg-stone-50 border-stone-50 dark:bg-stone-50 dark:hover:bg-stone-50" />
|
||||
<div className="text-stone-100 bg-stone-100 hover:bg-stone-100 border-stone-100 dark:bg-stone-100 dark:hover:bg-stone-100" />
|
||||
<div className="text-stone-200 bg-stone-200 hover:bg-stone-200 border-stone-200 dark:bg-stone-200 dark:hover:bg-stone-200" />
|
||||
<div className="text-stone-700 bg-stone-700 hover:bg-stone-700 border-stone-700 dark:bg-stone-700 dark:hover:bg-stone-700" />
|
||||
<div className="text-stone-800 bg-stone-800 hover:bg-stone-800 border-stone-800 dark:bg-stone-800 dark:hover:bg-stone-800" />
|
||||
<div className="text-stone-900 bg-stone-900 hover:bg-stone-900 border-stone-900 dark:bg-stone-900 dark:hover:bg-stone-900" />
|
||||
<div className="text-neutral-50 bg-neutral-50 hover:bg-neutral-50 border-neutral-50 dark:bg-neutral-50 dark:hover:bg-neutral-50" />
|
||||
<div className="text-neutral-100 bg-neutral-100 hover:bg-neutral-100 border-neutral-100 dark:bg-neutral-100 dark:hover:bg-neutral-100" />
|
||||
<div className="text-neutral-200 bg-neutral-200 hover:bg-neutral-200 border-neutral-200 dark:bg-neutral-200 dark:hover:bg-neutral-200" />
|
||||
<div className="text-neutral-700 bg-neutral-700 hover:bg-neutral-700 border-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-700" />
|
||||
<div className="text-neutral-800 bg-neutral-800 hover:bg-neutral-800 border-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800" />
|
||||
<div className="text-neutral-900 bg-neutral-900 hover:bg-neutral-900 border-neutral-900 dark:bg-neutral-900 dark:hover:bg-neutral-900" />
|
||||
<div className="text-zinc-50 bg-zinc-50 hover:bg-zinc-50 border-zinc-50 dark:bg-zinc-50 dark:hover:bg-zinc-50" />
|
||||
<div className="text-zinc-100 bg-zinc-100 hover:bg-zinc-100 border-zinc-100 dark:bg-zinc-100 dark:hover:bg-zinc-100" />
|
||||
<div className="text-zinc-200 bg-zinc-200 hover:bg-zinc-200 border-zinc-200 dark:bg-zinc-200 dark:hover:bg-zinc-200" />
|
||||
<div className="text-zinc-700 bg-zinc-700 hover:bg-zinc-700 border-zinc-700 dark:bg-zinc-700 dark:hover:bg-zinc-700" />
|
||||
<div className="text-zinc-800 bg-zinc-800 hover:bg-zinc-800 border-zinc-800 dark:bg-zinc-800 dark:hover:bg-zinc-800" />
|
||||
<div className="text-zinc-900 bg-zinc-900 hover:bg-zinc-900 border-zinc-900 dark:bg-zinc-900 dark:hover:bg-zinc-900" />
|
||||
<div className="text-grey-50 bg-grey-50 hover:bg-grey-50 border-grey-50 dark:bg-grey-50 dark:hover:bg-grey-50" />
|
||||
<div className="text-grey-100 bg-grey-100 hover:bg-grey-100 border-grey-100 dark:bg-grey-100 dark:hover:bg-grey-100" />
|
||||
<div className="text-grey-200 bg-grey-200 hover:bg-grey-200 border-grey-200 dark:bg-grey-200 dark:hover:bg-grey-200" />
|
||||
<div className="text-grey-700 bg-grey-700 hover:bg-grey-700 border-grey-700 dark:bg-grey-700 dark:hover:bg-grey-700" />
|
||||
<div className="text-grey-800 bg-grey-800 hover:bg-grey-800 border-grey-800 dark:bg-grey-800 dark:hover:bg-grey-800" />
|
||||
<div className="text-grey-900 bg-grey-900 hover:bg-grey-900 border-grey-900 dark:bg-grey-900 dark:hover:bg-grey-900" />
|
||||
<div className="text-slate-50 bg-slate-50 hover:bg-slate-50 border-slate-50 dark:bg-slate-50 dark:hover:bg-slate-50" />
|
||||
<div className="text-slate-100 bg-slate-100 hover:bg-slate-100 border-slate-100 dark:bg-slate-100 dark:hover:bg-slate-100" />
|
||||
<div className="text-slate-200 bg-slate-200 hover:bg-slate-200 border-slate-200 dark:bg-slate-200 dark:hover:bg-slate-200" />
|
||||
<div className="text-slate-700 bg-slate-700 hover:bg-slate-700 border-slate-700 dark:bg-slate-700 dark:hover:bg-slate-700" />
|
||||
<div className="text-slate-800 bg-slate-800 hover:bg-slate-800 border-slate-800 dark:bg-slate-800 dark:hover:bg-slate-800" />
|
||||
<div className="text-slate-900 bg-slate-900 hover:bg-slate-900 border-slate-900 dark:bg-slate-900 dark:hover:bg-slate-900" />
|
||||
<div className="bg-chart-0 text-chart-0" />
|
||||
<div className="bg-chart-1 text-chart-1" />
|
||||
<div className="bg-chart-2 text-chart-2" />
|
||||
<div className="bg-chart-3 text-chart-3" />
|
||||
<div className="bg-chart-4 text-chart-4" />
|
||||
<div className="bg-chart-5 text-chart-5" />
|
||||
<div className="bg-chart-6 text-chart-6" />
|
||||
<div className="bg-chart-7 text-chart-7" />
|
||||
<div className="bg-chart-8 text-chart-8" />
|
||||
<div className="bg-chart-9 text-chart-9" />
|
||||
<div className="bg-chart-10 text-chart-10" />
|
||||
<div className="bg-chart-11 text-chart-11" />
|
||||
<div className="border-rose-50 bg-rose-50 text-rose-50 hover:bg-rose-50 dark:bg-rose-50 dark:hover:bg-rose-50" />
|
||||
<div className="border-rose-100 bg-rose-100 text-rose-100 hover:bg-rose-100 dark:bg-rose-100 dark:hover:bg-rose-100" />
|
||||
<div className="border-rose-200 bg-rose-200 text-rose-200 hover:bg-rose-200 dark:bg-rose-200 dark:hover:bg-rose-200" />
|
||||
<div className="border-rose-700 bg-rose-700 text-rose-700 hover:bg-rose-700 dark:bg-rose-700 dark:hover:bg-rose-700" />
|
||||
<div className="border-rose-800 bg-rose-800 text-rose-800 hover:bg-rose-800 dark:bg-rose-800 dark:hover:bg-rose-800" />
|
||||
<div className="border-rose-900 bg-rose-900 text-rose-900 hover:bg-rose-900 dark:bg-rose-900 dark:hover:bg-rose-900" />
|
||||
<div className="border-pink-50 bg-pink-50 text-pink-50 hover:bg-pink-50 dark:bg-pink-50 dark:hover:bg-pink-50" />
|
||||
<div className="border-pink-100 bg-pink-100 text-pink-100 hover:bg-pink-100 dark:bg-pink-100 dark:hover:bg-pink-100" />
|
||||
<div className="border-pink-200 bg-pink-200 text-pink-200 hover:bg-pink-200 dark:bg-pink-200 dark:hover:bg-pink-200" />
|
||||
<div className="border-pink-700 bg-pink-700 text-pink-700 hover:bg-pink-700 dark:bg-pink-700 dark:hover:bg-pink-700" />
|
||||
<div className="border-pink-800 bg-pink-800 text-pink-800 hover:bg-pink-800 dark:bg-pink-800 dark:hover:bg-pink-800" />
|
||||
<div className="border-pink-900 bg-pink-900 text-pink-900 hover:bg-pink-900 dark:bg-pink-900 dark:hover:bg-pink-900" />
|
||||
<div className="border-fuchsia-50 bg-fuchsia-50 text-fuchsia-50 hover:bg-fuchsia-50 dark:bg-fuchsia-50 dark:hover:bg-fuchsia-50" />
|
||||
<div className="border-fuchsia-100 bg-fuchsia-100 text-fuchsia-100 hover:bg-fuchsia-100 dark:bg-fuchsia-100 dark:hover:bg-fuchsia-100" />
|
||||
<div className="border-fuchsia-200 bg-fuchsia-200 text-fuchsia-200 hover:bg-fuchsia-200 dark:bg-fuchsia-200 dark:hover:bg-fuchsia-200" />
|
||||
<div className="border-fuchsia-700 bg-fuchsia-700 text-fuchsia-700 hover:bg-fuchsia-700 dark:bg-fuchsia-700 dark:hover:bg-fuchsia-700" />
|
||||
<div className="border-fuchsia-800 bg-fuchsia-800 text-fuchsia-800 hover:bg-fuchsia-800 dark:bg-fuchsia-800 dark:hover:bg-fuchsia-800" />
|
||||
<div className="border-fuchsia-900 bg-fuchsia-900 text-fuchsia-900 hover:bg-fuchsia-900 dark:bg-fuchsia-900 dark:hover:bg-fuchsia-900" />
|
||||
<div className="border-purple-50 bg-purple-50 text-purple-50 hover:bg-purple-50 dark:bg-purple-50 dark:hover:bg-purple-50" />
|
||||
<div className="border-purple-100 bg-purple-100 text-purple-100 hover:bg-purple-100 dark:bg-purple-100 dark:hover:bg-purple-100" />
|
||||
<div className="border-purple-200 bg-purple-200 text-purple-200 hover:bg-purple-200 dark:bg-purple-200 dark:hover:bg-purple-200" />
|
||||
<div className="border-purple-700 bg-purple-700 text-purple-700 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-700" />
|
||||
<div className="border-purple-800 bg-purple-800 text-purple-800 hover:bg-purple-800 dark:bg-purple-800 dark:hover:bg-purple-800" />
|
||||
<div className="border-purple-900 bg-purple-900 text-purple-900 hover:bg-purple-900 dark:bg-purple-900 dark:hover:bg-purple-900" />
|
||||
<div className="border-violet-50 bg-violet-50 text-violet-50 hover:bg-violet-50 dark:bg-violet-50 dark:hover:bg-violet-50" />
|
||||
<div className="border-violet-100 bg-violet-100 text-violet-100 hover:bg-violet-100 dark:bg-violet-100 dark:hover:bg-violet-100" />
|
||||
<div className="border-violet-200 bg-violet-200 text-violet-200 hover:bg-violet-200 dark:bg-violet-200 dark:hover:bg-violet-200" />
|
||||
<div className="border-violet-700 bg-violet-700 text-violet-700 hover:bg-violet-700 dark:bg-violet-700 dark:hover:bg-violet-700" />
|
||||
<div className="border-violet-800 bg-violet-800 text-violet-800 hover:bg-violet-800 dark:bg-violet-800 dark:hover:bg-violet-800" />
|
||||
<div className="border-violet-900 bg-violet-900 text-violet-900 hover:bg-violet-900 dark:bg-violet-900 dark:hover:bg-violet-900" />
|
||||
<div className="border-indigo-50 bg-indigo-50 text-indigo-50 hover:bg-indigo-50 dark:bg-indigo-50 dark:hover:bg-indigo-50" />
|
||||
<div className="border-indigo-100 bg-indigo-100 text-indigo-100 hover:bg-indigo-100 dark:bg-indigo-100 dark:hover:bg-indigo-100" />
|
||||
<div className="border-indigo-200 bg-indigo-200 text-indigo-200 hover:bg-indigo-200 dark:bg-indigo-200 dark:hover:bg-indigo-200" />
|
||||
<div className="border-indigo-700 bg-indigo-700 text-indigo-700 hover:bg-indigo-700 dark:bg-indigo-700 dark:hover:bg-indigo-700" />
|
||||
<div className="border-indigo-800 bg-indigo-800 text-indigo-800 hover:bg-indigo-800 dark:bg-indigo-800 dark:hover:bg-indigo-800" />
|
||||
<div className="border-indigo-900 bg-indigo-900 text-indigo-900 hover:bg-indigo-900 dark:bg-indigo-900 dark:hover:bg-indigo-900" />
|
||||
<div className="border-blue-50 bg-blue-50 text-blue-50 hover:bg-blue-50 dark:bg-blue-50 dark:hover:bg-blue-50" />
|
||||
<div className="border-blue-100 bg-blue-100 text-blue-100 hover:bg-blue-100 dark:bg-blue-100 dark:hover:bg-blue-100" />
|
||||
<div className="border-blue-200 bg-blue-200 text-blue-200 hover:bg-blue-200 dark:bg-blue-200 dark:hover:bg-blue-200" />
|
||||
<div className="border-blue-700 bg-blue-700 text-blue-700 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-700" />
|
||||
<div className="border-blue-800 bg-blue-800 text-blue-800 hover:bg-blue-800 dark:bg-blue-800 dark:hover:bg-blue-800" />
|
||||
<div className="border-blue-900 bg-blue-900 text-blue-900 hover:bg-blue-900 dark:bg-blue-900 dark:hover:bg-blue-900" />
|
||||
<div className="border-sky-50 bg-sky-50 text-sky-50 hover:bg-sky-50 dark:bg-sky-50 dark:hover:bg-sky-50" />
|
||||
<div className="border-sky-100 bg-sky-100 text-sky-100 hover:bg-sky-100 dark:bg-sky-100 dark:hover:bg-sky-100" />
|
||||
<div className="border-sky-200 bg-sky-200 text-sky-200 hover:bg-sky-200 dark:bg-sky-200 dark:hover:bg-sky-200" />
|
||||
<div className="border-sky-700 bg-sky-700 text-sky-700 hover:bg-sky-700 dark:bg-sky-700 dark:hover:bg-sky-700" />
|
||||
<div className="border-sky-800 bg-sky-800 text-sky-800 hover:bg-sky-800 dark:bg-sky-800 dark:hover:bg-sky-800" />
|
||||
<div className="border-sky-900 bg-sky-900 text-sky-900 hover:bg-sky-900 dark:bg-sky-900 dark:hover:bg-sky-900" />
|
||||
<div className="border-cyan-50 bg-cyan-50 text-cyan-50 hover:bg-cyan-50 dark:bg-cyan-50 dark:hover:bg-cyan-50" />
|
||||
<div className="border-cyan-100 bg-cyan-100 text-cyan-100 hover:bg-cyan-100 dark:bg-cyan-100 dark:hover:bg-cyan-100" />
|
||||
<div className="border-cyan-200 bg-cyan-200 text-cyan-200 hover:bg-cyan-200 dark:bg-cyan-200 dark:hover:bg-cyan-200" />
|
||||
<div className="border-cyan-700 bg-cyan-700 text-cyan-700 hover:bg-cyan-700 dark:bg-cyan-700 dark:hover:bg-cyan-700" />
|
||||
<div className="border-cyan-800 bg-cyan-800 text-cyan-800 hover:bg-cyan-800 dark:bg-cyan-800 dark:hover:bg-cyan-800" />
|
||||
<div className="border-cyan-900 bg-cyan-900 text-cyan-900 hover:bg-cyan-900 dark:bg-cyan-900 dark:hover:bg-cyan-900" />
|
||||
<div className="border-teal-50 bg-teal-50 text-teal-50 hover:bg-teal-50 dark:bg-teal-50 dark:hover:bg-teal-50" />
|
||||
<div className="border-teal-100 bg-teal-100 text-teal-100 hover:bg-teal-100 dark:bg-teal-100 dark:hover:bg-teal-100" />
|
||||
<div className="border-teal-200 bg-teal-200 text-teal-200 hover:bg-teal-200 dark:bg-teal-200 dark:hover:bg-teal-200" />
|
||||
<div className="border-teal-700 bg-teal-700 text-teal-700 hover:bg-teal-700 dark:bg-teal-700 dark:hover:bg-teal-700" />
|
||||
<div className="border-teal-800 bg-teal-800 text-teal-800 hover:bg-teal-800 dark:bg-teal-800 dark:hover:bg-teal-800" />
|
||||
<div className="border-teal-900 bg-teal-900 text-teal-900 hover:bg-teal-900 dark:bg-teal-900 dark:hover:bg-teal-900" />
|
||||
<div className="border-emerald-50 bg-emerald-50 text-emerald-50 hover:bg-emerald-50 dark:bg-emerald-50 dark:hover:bg-emerald-50" />
|
||||
<div className="border-emerald-100 bg-emerald-100 text-emerald-100 hover:bg-emerald-100 dark:bg-emerald-100 dark:hover:bg-emerald-100" />
|
||||
<div className="border-emerald-200 bg-emerald-200 text-emerald-200 hover:bg-emerald-200 dark:bg-emerald-200 dark:hover:bg-emerald-200" />
|
||||
<div className="border-emerald-700 bg-emerald-700 text-emerald-700 hover:bg-emerald-700 dark:bg-emerald-700 dark:hover:bg-emerald-700" />
|
||||
<div className="border-emerald-800 bg-emerald-800 text-emerald-800 hover:bg-emerald-800 dark:bg-emerald-800 dark:hover:bg-emerald-800" />
|
||||
<div className="border-emerald-900 bg-emerald-900 text-emerald-900 hover:bg-emerald-900 dark:bg-emerald-900 dark:hover:bg-emerald-900" />
|
||||
<div className="border-green-50 bg-green-50 text-green-50 hover:bg-green-50 dark:bg-green-50 dark:hover:bg-green-50" />
|
||||
<div className="border-green-100 bg-green-100 text-green-100 hover:bg-green-100 dark:bg-green-100 dark:hover:bg-green-100" />
|
||||
<div className="border-green-200 bg-green-200 text-green-200 hover:bg-green-200 dark:bg-green-200 dark:hover:bg-green-200" />
|
||||
<div className="border-green-700 bg-green-700 text-green-700 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-700" />
|
||||
<div className="border-green-800 bg-green-800 text-green-800 hover:bg-green-800 dark:bg-green-800 dark:hover:bg-green-800" />
|
||||
<div className="border-green-900 bg-green-900 text-green-900 hover:bg-green-900 dark:bg-green-900 dark:hover:bg-green-900" />
|
||||
<div className="border-lime-50 bg-lime-50 text-lime-50 hover:bg-lime-50 dark:bg-lime-50 dark:hover:bg-lime-50" />
|
||||
<div className="border-lime-100 bg-lime-100 text-lime-100 hover:bg-lime-100 dark:bg-lime-100 dark:hover:bg-lime-100" />
|
||||
<div className="border-lime-200 bg-lime-200 text-lime-200 hover:bg-lime-200 dark:bg-lime-200 dark:hover:bg-lime-200" />
|
||||
<div className="border-lime-700 bg-lime-700 text-lime-700 hover:bg-lime-700 dark:bg-lime-700 dark:hover:bg-lime-700" />
|
||||
<div className="border-lime-800 bg-lime-800 text-lime-800 hover:bg-lime-800 dark:bg-lime-800 dark:hover:bg-lime-800" />
|
||||
<div className="border-lime-900 bg-lime-900 text-lime-900 hover:bg-lime-900 dark:bg-lime-900 dark:hover:bg-lime-900" />
|
||||
<div className="border-yellow-50 bg-yellow-50 text-yellow-50 hover:bg-yellow-50 dark:bg-yellow-50 dark:hover:bg-yellow-50" />
|
||||
<div className="border-yellow-100 bg-yellow-100 text-yellow-100 hover:bg-yellow-100 dark:bg-yellow-100 dark:hover:bg-yellow-100" />
|
||||
<div className="border-yellow-200 bg-yellow-200 text-yellow-200 hover:bg-yellow-200 dark:bg-yellow-200 dark:hover:bg-yellow-200" />
|
||||
<div className="border-yellow-700 bg-yellow-700 text-yellow-700 hover:bg-yellow-700 dark:bg-yellow-700 dark:hover:bg-yellow-700" />
|
||||
<div className="border-yellow-800 bg-yellow-800 text-yellow-800 hover:bg-yellow-800 dark:bg-yellow-800 dark:hover:bg-yellow-800" />
|
||||
<div className="border-yellow-900 bg-yellow-900 text-yellow-900 hover:bg-yellow-900 dark:bg-yellow-900 dark:hover:bg-yellow-900" />
|
||||
<div className="border-amber-50 bg-amber-50 text-amber-50 hover:bg-amber-50 dark:bg-amber-50 dark:hover:bg-amber-50" />
|
||||
<div className="border-amber-100 bg-amber-100 text-amber-100 hover:bg-amber-100 dark:bg-amber-100 dark:hover:bg-amber-100" />
|
||||
<div className="border-amber-200 bg-amber-200 text-amber-200 hover:bg-amber-200 dark:bg-amber-200 dark:hover:bg-amber-200" />
|
||||
<div className="border-amber-700 bg-amber-700 text-amber-700 hover:bg-amber-700 dark:bg-amber-700 dark:hover:bg-amber-700" />
|
||||
<div className="border-amber-800 bg-amber-800 text-amber-800 hover:bg-amber-800 dark:bg-amber-800 dark:hover:bg-amber-800" />
|
||||
<div className="border-amber-900 bg-amber-900 text-amber-900 hover:bg-amber-900 dark:bg-amber-900 dark:hover:bg-amber-900" />
|
||||
<div className="border-orange-50 bg-orange-50 text-orange-50 hover:bg-orange-50 dark:bg-orange-50 dark:hover:bg-orange-50" />
|
||||
<div className="border-orange-100 bg-orange-100 text-orange-100 hover:bg-orange-100 dark:bg-orange-100 dark:hover:bg-orange-100" />
|
||||
<div className="border-orange-200 bg-orange-200 text-orange-200 hover:bg-orange-200 dark:bg-orange-200 dark:hover:bg-orange-200" />
|
||||
<div className="border-orange-700 bg-orange-700 text-orange-700 hover:bg-orange-700 dark:bg-orange-700 dark:hover:bg-orange-700" />
|
||||
<div className="border-orange-800 bg-orange-800 text-orange-800 hover:bg-orange-800 dark:bg-orange-800 dark:hover:bg-orange-800" />
|
||||
<div className="border-orange-900 bg-orange-900 text-orange-900 hover:bg-orange-900 dark:bg-orange-900 dark:hover:bg-orange-900" />
|
||||
<div className="border-red-50 bg-red-50 text-red-50 hover:bg-red-50 dark:bg-red-50 dark:hover:bg-red-50" />
|
||||
<div className="border-red-100 bg-red-100 text-red-100 hover:bg-red-100 dark:bg-red-100 dark:hover:bg-red-100" />
|
||||
<div className="border-red-200 bg-red-200 text-red-200 hover:bg-red-200 dark:bg-red-200 dark:hover:bg-red-200" />
|
||||
<div className="border-red-700 bg-red-700 text-red-700 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-700" />
|
||||
<div className="border-red-800 bg-red-800 text-red-800 hover:bg-red-800 dark:bg-red-800 dark:hover:bg-red-800" />
|
||||
<div className="border-red-900 bg-red-900 text-red-900 hover:bg-red-900 dark:bg-red-900 dark:hover:bg-red-900" />
|
||||
<div className="border-stone-50 bg-stone-50 text-stone-50 hover:bg-stone-50 dark:bg-stone-50 dark:hover:bg-stone-50" />
|
||||
<div className="border-stone-100 bg-stone-100 text-stone-100 hover:bg-stone-100 dark:bg-stone-100 dark:hover:bg-stone-100" />
|
||||
<div className="border-stone-200 bg-stone-200 text-stone-200 hover:bg-stone-200 dark:bg-stone-200 dark:hover:bg-stone-200" />
|
||||
<div className="border-stone-700 bg-stone-700 text-stone-700 hover:bg-stone-700 dark:bg-stone-700 dark:hover:bg-stone-700" />
|
||||
<div className="border-stone-800 bg-stone-800 text-stone-800 hover:bg-stone-800 dark:bg-stone-800 dark:hover:bg-stone-800" />
|
||||
<div className="border-stone-900 bg-stone-900 text-stone-900 hover:bg-stone-900 dark:bg-stone-900 dark:hover:bg-stone-900" />
|
||||
<div className="border-neutral-50 bg-neutral-50 text-neutral-50 hover:bg-neutral-50 dark:bg-neutral-50 dark:hover:bg-neutral-50" />
|
||||
<div className="border-neutral-100 bg-neutral-100 text-neutral-100 hover:bg-neutral-100 dark:bg-neutral-100 dark:hover:bg-neutral-100" />
|
||||
<div className="border-neutral-200 bg-neutral-200 text-neutral-200 hover:bg-neutral-200 dark:bg-neutral-200 dark:hover:bg-neutral-200" />
|
||||
<div className="border-neutral-700 bg-neutral-700 text-neutral-700 hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-700" />
|
||||
<div className="border-neutral-800 bg-neutral-800 text-neutral-800 hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800" />
|
||||
<div className="border-neutral-900 bg-neutral-900 text-neutral-900 hover:bg-neutral-900 dark:bg-neutral-900 dark:hover:bg-neutral-900" />
|
||||
<div className="border-zinc-50 bg-zinc-50 text-zinc-50 hover:bg-zinc-50 dark:bg-zinc-50 dark:hover:bg-zinc-50" />
|
||||
<div className="border-zinc-100 bg-zinc-100 text-zinc-100 hover:bg-zinc-100 dark:bg-zinc-100 dark:hover:bg-zinc-100" />
|
||||
<div className="border-zinc-200 bg-zinc-200 text-zinc-200 hover:bg-zinc-200 dark:bg-zinc-200 dark:hover:bg-zinc-200" />
|
||||
<div className="border-zinc-700 bg-zinc-700 text-zinc-700 hover:bg-zinc-700 dark:bg-zinc-700 dark:hover:bg-zinc-700" />
|
||||
<div className="border-zinc-800 bg-zinc-800 text-zinc-800 hover:bg-zinc-800 dark:bg-zinc-800 dark:hover:bg-zinc-800" />
|
||||
<div className="border-zinc-900 bg-zinc-900 text-zinc-900 hover:bg-zinc-900 dark:bg-zinc-900 dark:hover:bg-zinc-900" />
|
||||
<div className="border-grey-50 bg-grey-50 text-grey-50 hover:bg-grey-50 dark:bg-grey-50 dark:hover:bg-grey-50" />
|
||||
<div className="border-grey-100 bg-grey-100 text-grey-100 hover:bg-grey-100 dark:bg-grey-100 dark:hover:bg-grey-100" />
|
||||
<div className="border-grey-200 bg-grey-200 text-grey-200 hover:bg-grey-200 dark:bg-grey-200 dark:hover:bg-grey-200" />
|
||||
<div className="border-grey-700 bg-grey-700 text-grey-700 hover:bg-grey-700 dark:bg-grey-700 dark:hover:bg-grey-700" />
|
||||
<div className="border-grey-800 bg-grey-800 text-grey-800 hover:bg-grey-800 dark:bg-grey-800 dark:hover:bg-grey-800" />
|
||||
<div className="border-grey-900 bg-grey-900 text-grey-900 hover:bg-grey-900 dark:bg-grey-900 dark:hover:bg-grey-900" />
|
||||
<div className="border-slate-50 bg-slate-50 text-slate-50 hover:bg-slate-50 dark:bg-slate-50 dark:hover:bg-slate-50" />
|
||||
<div className="border-slate-100 bg-slate-100 text-slate-100 hover:bg-slate-100 dark:bg-slate-100 dark:hover:bg-slate-100" />
|
||||
<div className="border-slate-200 bg-slate-200 text-slate-200 hover:bg-slate-200 dark:bg-slate-200 dark:hover:bg-slate-200" />
|
||||
<div className="border-slate-700 bg-slate-700 text-slate-700 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-700" />
|
||||
<div className="border-slate-800 bg-slate-800 text-slate-800 hover:bg-slate-800 dark:bg-slate-800 dark:hover:bg-slate-800" />
|
||||
<div className="border-slate-900 bg-slate-900 text-slate-900 hover:bg-slate-900 dark:bg-slate-900 dark:hover:bg-slate-900" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Sidebar } from '@/components/sidebar';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { getServerEnvs } from '@/server/get-envs';
|
||||
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';
|
||||
|
||||
export interface ConfigResonse {
|
||||
apiUrl: string;
|
||||
dashboardUrl: string;
|
||||
isSelfHosted: boolean;
|
||||
isMaintenance: boolean;
|
||||
isDemo: boolean;
|
||||
}
|
||||
// Nothing sensitive here, its client environment variables which is good for debugging
|
||||
export const Route = createFileRoute('/api/config')({
|
||||
server: {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
|
||||
export const getServerEnvs = createServerFn().handler(async () => {
|
||||
export const getServerEnvs = createServerFn().handler(() => {
|
||||
const envs = {
|
||||
apiUrl: String(process.env.API_URL || process.env.NEXT_PUBLIC_API_URL),
|
||||
dashboardUrl: String(
|
||||
process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL,
|
||||
process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL
|
||||
),
|
||||
isSelfHosted: process.env.SELF_HOSTED !== undefined,
|
||||
isMaintenance: process.env.MAINTENANCE === '1',
|
||||
isDemo: process.env.DEMO_USER_ID !== undefined,
|
||||
};
|
||||
|
||||
return envs;
|
||||
|
||||
Reference in New Issue
Block a user