Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions web/app/components/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const productionNavigation = {
products,
resources: [
{ key: 'docs', href: DOCS_URL, internal: false },
{ key: 'pricing', href: `${routesPath.main}#pricing`, internal: true },
{ key: 'pricing', href: routesPath.pricing, internal: true },
{ key: 'tools', href: routesPath.tools, internal: true },
{ key: 'utm', href: routesPath.utm_generator, internal: true },
{ key: 'ctr', href: routesPath.ctr_calculator, internal: true },
Expand Down Expand Up @@ -81,7 +81,7 @@ const communityEditionNavigation = {
products,
resources: [
{ key: 'docs', href: DOCS_URL, internal: false },
{ key: 'pricing', href: `https://swetrix.com/#pricing`, internal: false },
{ key: 'pricing', href: `https://swetrix.com/pricing`, internal: false },
{ key: 'tools', href: `https://swetrix.com${routesPath.tools}`, internal: false },
{ key: 'utm', href: `https://swetrix.com${routesPath.utm_generator}`, internal: false },
{ key: 'ctr', href: `https://swetrix.com${routesPath.ctr_calculator}`, internal: false },
Expand Down
4 changes: 2 additions & 2 deletions web/app/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ const NotAuthedHeader = ({
) : null}
{!isSelfhosted && !isDisableMarketingPages ? (
<Link
to={`${routes.main}#pricing`}
to={routes.pricing}
className='underline-animate text-base leading-6 font-semibold text-slate-800 focus:outline-hidden dark:text-white'
key='Pricing'
>
Expand Down Expand Up @@ -940,7 +940,7 @@ const Header = ({ refPage, transparent }: HeaderProps) => {
) : null}
{!isSelfhosted && !isDisableMarketingPages && !isAuthenticated ? (
<Link
to={`${routes.main}#pricing`}
to={routes.pricing}
onClick={() => setMobileMenuOpen(false)}
className='-mx-3 block rounded-lg px-3 py-2 text-base leading-7 font-semibold text-gray-900 transition-colors hover:bg-gray-400/20 dark:text-gray-50 dark:hover:bg-slate-700/50'
key='Pricing'
Expand Down
200 changes: 200 additions & 0 deletions web/app/components/pricing/FeaturesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { CheckIcon, MinusIcon } from '@heroicons/react/24/solid'
import cx from 'clsx'
import React from 'react'
import { useTranslation } from 'react-i18next'

type FeatureValue = boolean | string

interface Feature {
name: string
standard: FeatureValue
enterprise: FeatureValue
}

interface FeatureCategory {
category: string
features: Feature[]
}

const FEATURE_DATA: FeatureCategory[] = [
{
category: 'pricing.features.analytics',
features: [
{ name: 'pricing.features.trafficAnalytics', standard: true, enterprise: true },
{ name: 'pricing.features.customEvents', standard: true, enterprise: true },
{ name: 'pricing.features.performanceMonitoring', standard: true, enterprise: true },
{ name: 'pricing.features.errorTracking', standard: true, enterprise: true },
{ name: 'pricing.features.funnels', standard: true, enterprise: true },
{ name: 'pricing.features.sessionAnalysis', standard: true, enterprise: true },
{ name: 'pricing.features.userFlow', standard: true, enterprise: true },
{ name: 'pricing.features.annotations', standard: true, enterprise: true },
{ name: 'pricing.features.customAlerts', standard: true, enterprise: true },
{ name: 'pricing.features.emailReports', standard: true, enterprise: true },
],
},
{
category: 'pricing.features.dataManagement',
features: [
{
name: 'pricing.features.dataRetention',
standard: 'pricing.features.unlimited',
enterprise: 'pricing.features.unlimited',
},
{ name: 'pricing.features.dataExport', standard: true, enterprise: true },
{ name: 'pricing.features.dataOwnership', standard: true, enterprise: true },
{ name: 'pricing.features.gdprCompliant', standard: true, enterprise: true },
],
},
{
category: 'pricing.features.collaboration',
features: [
{
name: 'pricing.features.teamMembers',
standard: 'pricing.features.unlimited',
enterprise: 'pricing.features.unlimited',
},
{ name: 'pricing.features.projectSharing', standard: true, enterprise: true },
{ name: 'pricing.features.publicDashboards', standard: true, enterprise: true },
{ name: 'pricing.features.organisations', standard: true, enterprise: true },
],
},
{
category: 'pricing.features.apiAccess',
features: [
{ name: 'pricing.features.statisticsApi', standard: true, enterprise: true },
{ name: 'pricing.features.eventsApi', standard: true, enterprise: true },
{ name: 'pricing.features.adminApi', standard: true, enterprise: true },
],
},
{
category: 'pricing.features.security',
features: [
{ name: 'pricing.features.twoFactorAuth', standard: true, enterprise: true },
{ name: 'pricing.features.ssoSaml', standard: false, enterprise: true },
{ name: 'pricing.features.auditLogs', standard: false, enterprise: true },
{ name: 'pricing.features.roleBasedAccess', standard: true, enterprise: true },
],
},
{
category: 'pricing.features.supportCategory',
features: [
{ name: 'pricing.features.communitySupport', standard: true, enterprise: true },
{ name: 'pricing.features.emailSupport', standard: true, enterprise: true },
{ name: 'pricing.features.prioritySupport', standard: false, enterprise: true },
{ name: 'pricing.features.slackSupport', standard: false, enterprise: true },
{ name: 'pricing.features.dedicatedManager', standard: false, enterprise: true },
{ name: 'pricing.features.manualInvoicing', standard: false, enterprise: true },
{ name: 'pricing.features.uptimeSla', standard: false, enterprise: 'pricing.features.slaValue' },
{ name: 'pricing.features.customContracts', standard: false, enterprise: true },
],
},
{
category: 'pricing.features.deployment',
features: [
{ name: 'pricing.features.cloudHosted', standard: true, enterprise: true },
{ name: 'pricing.features.selfHosted', standard: true, enterprise: true },
{ name: 'pricing.features.onPremise', standard: false, enterprise: true },
{ name: 'pricing.features.customIntegrations', standard: false, enterprise: true },
],
},
]

const FeatureValueCell = ({ value }: { value: FeatureValue }) => {
const { t } = useTranslation('common')

if (typeof value === 'boolean') {
return value ? (
<CheckIcon className='mx-auto size-5 text-emerald-500' aria-label={t('common.yes')} />
) : (
<MinusIcon className='mx-auto size-5 text-gray-400 dark:text-gray-500' aria-label={t('common.no')} />
)
}

return <span className='text-sm font-medium text-slate-700 dark:text-gray-200'>{t(value)}</span>
}

interface FeaturesTableProps {
className?: string
}

const FeaturesTable = ({ className }: FeaturesTableProps) => {
const { t } = useTranslation('common')

return (
<section className={cx('py-16 sm:py-20', className)}>
<div className='mx-auto max-w-5xl px-4 sm:px-6 lg:px-8'>
<div className='text-center'>
<h2 className='text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white'>
{t('pricing.features.title')}
</h2>
<p className='mx-auto mt-4 max-w-2xl text-base text-slate-600 dark:text-gray-300'>
{t('pricing.features.subtitle')}
</p>
</div>

<div className='mt-12 overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-slate-700 dark:bg-slate-800/50'>
<table className='w-full'>
<thead>
<tr className='border-b border-gray-200 bg-gray-50 dark:border-slate-700 dark:bg-slate-800'>
<th className='px-4 py-4 text-left text-sm font-semibold text-slate-900 sm:px-6 dark:text-white'>
{t('pricing.features.feature')}
</th>
<th className='w-32 px-4 py-4 text-center text-sm font-semibold text-slate-900 sm:w-40 sm:px-6 dark:text-white'>
{t('pricing.features.standard')}
</th>
<th className='w-32 px-4 py-4 text-center text-sm font-semibold text-slate-900 sm:w-40 sm:px-6 dark:text-white'>
{t('pricing.features.enterprise')}
</th>
</tr>
</thead>
<tbody>
{FEATURE_DATA.map((category) => (
<React.Fragment key={category.category}>
<tr className='border-t border-gray-200 bg-slate-50/50 dark:border-slate-700 dark:bg-slate-800/80'>
<td
colSpan={3}
className='px-4 py-3 text-sm font-semibold tracking-wide text-indigo-600 uppercase sm:px-6 dark:text-indigo-400'
>
{t(category.category)}
</td>
</tr>
{category.features.map((feature, featureIndex) => (
<tr
key={feature.name}
className={cx(
'border-t border-gray-100 dark:border-slate-700/50',
featureIndex % 2 === 0 ? 'bg-white dark:bg-slate-800/30' : 'bg-gray-50/50 dark:bg-slate-800/50',
)}
>
<td className='px-4 py-3 text-sm text-slate-700 sm:px-6 dark:text-gray-300'>{t(feature.name)}</td>
<td className='px-4 py-3 text-center sm:px-6'>
<FeatureValueCell value={feature.standard} />
</td>
<td className='px-4 py-3 text-center sm:px-6'>
<FeatureValueCell value={feature.enterprise} />
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>

<div className='mt-8 text-center'>
<p className='text-sm text-slate-500 dark:text-gray-400'>
{t('pricing.features.needMore')}{' '}
<a
href='/contact'
className='font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300'
>
{t('pricing.features.contactUs')}
</a>
</p>
</div>
</div>
</section>
)
}

export default FeaturesTable
32 changes: 32 additions & 0 deletions web/app/routes/pricing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { redirect } from 'react-router'
import type { SitemapFunction } from 'remix-sitemap'

import FAQ from '~/components/marketing/FAQ'
import MarketingPricing from '~/components/pricing/MarketingPricing'
import FeaturesTable from '~/components/pricing/FeaturesTable'

Check failure on line 6 in web/app/routes/pricing.tsx

View workflow job for this annotation

GitHub Actions / Web Checks (22.x.x)

`~/components/pricing/FeaturesTable` import should occur before import of `~/components/pricing/MarketingPricing`
import { isSelfhosted, isDisableMarketingPages } from '~/lib/constants'

export const sitemap: SitemapFunction = () => ({
priority: 0.9,
exclude: isSelfhosted || isDisableMarketingPages,
})

export async function loader() {
if (isSelfhosted || isDisableMarketingPages) {
return redirect('/dashboard', 302)
}

return null
}

export default function PricingPage() {
return (
<div className='min-h-min-footer bg-gray-50 dark:bg-slate-900'>
<MarketingPricing />

<FeaturesTable />

<FAQ />
</div>
)
}
1 change: 1 addition & 0 deletions web/app/utils/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const routes = Object.freeze({
signin: '/login',
signup: '/signup',
pricing: '/pricing',
performance: '/performance',
errorTracking: '/error-tracking',
onboarding: '/onboarding',
Expand Down
6 changes: 6 additions & 0 deletions web/app/utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ export const getPageMeta = (t: typeof i18next.t, url?: string, _pathname?: strin
}
break

case routes.pricing:
result = {
title: t('titles.pricing'),
}
break

case routes.performance:
result = {
title: t('titles.performance'),
Expand Down
58 changes: 57 additions & 1 deletion web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1418,7 +1418,62 @@
"downgradeTitle": "Are you sure you want to downgrade?",
"downgradeDesc": "After downgrading, your plan will include fewer projects and available events per month. In case your website contains more traffic than the plan you want to downgrade to includes, this will result in not all events being stored in our database.\nYou will be able to return to your current plan at any time.\nIf you have any questions, you can always email us at {{email}}.",
"cancelTitle": "Are you sure you want to cancel your subscription?",
"cancelDesc": "After cancelling your subscription, no new events will be collected by Swetrix for your projects.\nYou will be able to return to your current plan at any time.\nIf you have any questions, you can always email us at {{email}}."
"cancelDesc": "After cancelling your subscription, no new events will be collected by Swetrix for your projects.\nYou will be able to return to your current plan at any time.\nIf you have any questions, you can always email us at {{email}}.",
"features": {
"title": "Compare plans",
"subtitle": "Everything you need is included in every plan. Enterprise adds priority support and advanced security features.",
"feature": "Feature",
"standard": "Standard",
"enterprise": "Enterprise",
"analytics": "Analytics",
"trafficAnalytics": "Traffic analytics",
"customEvents": "Custom events",
"performanceMonitoring": "Website speed analytics",
"errorTracking": "Error tracking",
"funnels": "Funnels",
"sessionAnalysis": "Session analysis",
"userFlow": "User flow",
"annotations": "Annotations",
"customAlerts": "Custom alerts",
"emailReports": "Email reports",
"dataManagement": "Data Management",
"dataRetention": "Data retention",
"unlimited": "Unlimited",
"dataExport": "Data export",
"dataOwnership": "100% data ownership",
"gdprCompliant": "GDPR compliant",
"collaboration": "Collaboration",
"teamMembers": "Team members",
"projectSharing": "Project sharing",
"publicDashboards": "Public dashboards",
"organisations": "Organisations",
"apiAccess": "API Access",
"statisticsApi": "Statistics API",
"eventsApi": "Events API",
"adminApi": "Admin API",
"security": "Security",
"twoFactorAuth": "Two-factor authentication (2FA)",
"ssoSaml": "SSO / SAML",
"auditLogs": "Audit logs",
"roleBasedAccess": "Role-based access control",
"supportCategory": "Support",
"communitySupport": "Community support (Discord)",
"emailSupport": "Email support",
"prioritySupport": "Priority support",
"slackSupport": "Dedicated Slack channel",
"dedicatedManager": "Dedicated account manager",
"manualInvoicing": "Manual invoicing",
"uptimeSla": "Uptime SLA",
"slaValue": "99.9%",
"customContracts": "Custom contracts",
"deployment": "Deployment",
"cloudHosted": "Cloud hosted",
"selfHosted": "Self-hosted",
"onPremise": "On-premise deployment",
"customIntegrations": "Custom integrations",
"needMore": "Need something more?",
"contactUs": "Contact us"
}
},
"contact": {
"description": "You can reach us at <mail>{{email}}</mail>, tweet us at <twitter>{{twitterHandle}}</twitter> or talk to us via our <discord>Discord</discord> community. We aim to respond as soon as possible, usually within a day.",
Expand Down Expand Up @@ -1502,6 +1557,7 @@
"organisations": "Organisations",
"docs": "Documentation",
"billing": "Billing",
"pricing": "Pricing",
"contact": "Have something to ask?",
"main": "Turn traffic into insights",
"socialisation": "Socialisation",
Expand Down
Loading