Add Analytics package (#46)
* Add Analytics package Created a new analytics package with a manager to handle tracking of events and page views. The package includes a default provider that can be switched out and uses a NullAnalyticsService if no provider is registered. Additional types, scripts, and package configuration are also provided to support development. * Add marketing components for UI package Introduced new React components under "marketing" for the UI package. These include 'Pill', 'GradientSecondaryText', 'Hero', 'CtaButton', 'FeatureCard', 'FeatureGrid', 'FeatureShowcase', 'GradientText', 'Header', and 'SecondaryHero'. Updated 'package.json' to export these components. Replaced the implementation of 'Home', 'SiteHeader', and 'SiteFooter' with these components for cleaner code and better reusability.
This commit is contained in:
committed by
GitHub
parent
5ee7bacb2a
commit
86d82d889c
@@ -1,5 +1,4 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Footer } from '@kit/ui/marketing';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
@@ -7,133 +6,53 @@ import appConfig from '~/config/app.config';
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className={'site-footer relative mt-auto w-full py-8 2xl:py-16'}>
|
||||
<div className={'container'}>
|
||||
<div className={'flex flex-col space-y-8 lg:flex-row lg:space-y-0'}>
|
||||
<div
|
||||
className={
|
||||
'flex w-full space-x-2 lg:w-4/12 xl:w-4/12' +
|
||||
' xl:space-x-6 2xl:space-x-8'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div>
|
||||
<AppLogo className={'w-[85px] md:w-[95px]'} />
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div>
|
||||
<p className={'text-sm text-muted-foreground'}>
|
||||
<Trans i18nKey={'marketing:footerDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex text-xs text-muted-foreground'}>
|
||||
<p>
|
||||
<Footer
|
||||
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
|
||||
description={<Trans i18nKey="marketing:footerDescription" />}
|
||||
copyright={
|
||||
<Trans
|
||||
i18nKey={'marketing:copyright'}
|
||||
i18nKey="marketing:copyright"
|
||||
values={{
|
||||
product: appConfig.name,
|
||||
year: new Date().getFullYear(),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
'flex flex-col space-y-8 lg:space-x-6 lg:space-y-0' +
|
||||
' w-full lg:flex-row lg:justify-end xl:space-x-16'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className={'flex flex-col space-y-2.5'}>
|
||||
<FooterSectionHeading>
|
||||
<Trans i18nKey={'marketing:about'} />
|
||||
</FooterSectionHeading>
|
||||
|
||||
<FooterSectionList>
|
||||
<FooterLink>
|
||||
<Link href={'/blog'}>
|
||||
<Trans i18nKey={'marketing:blog'} />
|
||||
</Link>
|
||||
</FooterLink>
|
||||
<FooterLink>
|
||||
<Link href={'/contact'}>
|
||||
<Trans i18nKey={'marketing:contact'} />
|
||||
</Link>
|
||||
</FooterLink>
|
||||
</FooterSectionList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={'flex flex-col space-y-2.5'}>
|
||||
<FooterSectionHeading>
|
||||
<Trans i18nKey={'marketing:product'} />
|
||||
</FooterSectionHeading>
|
||||
|
||||
<FooterSectionList>
|
||||
<FooterLink>
|
||||
<Link href={'/docs'}>
|
||||
<Trans i18nKey={'marketing:documentation'} />
|
||||
</Link>
|
||||
</FooterLink>
|
||||
</FooterSectionList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={'flex flex-col space-y-2.5'}>
|
||||
<FooterSectionHeading>
|
||||
<Trans i18nKey={'marketing:legal'} />
|
||||
</FooterSectionHeading>
|
||||
|
||||
<FooterSectionList>
|
||||
<FooterLink>
|
||||
<Link href={'/terms-of-service'}>
|
||||
<Trans i18nKey={'marketing:termsOfService'} />
|
||||
</Link>
|
||||
</FooterLink>
|
||||
<FooterLink>
|
||||
<Link href={'/privacy-policy'}>
|
||||
<Trans i18nKey={'marketing:privacyPolicy'} />
|
||||
</Link>
|
||||
</FooterLink>
|
||||
<FooterLink>
|
||||
<Link href={'/cookie-policy'}>
|
||||
<Trans i18nKey={'marketing:cookiePolicy'} />
|
||||
</Link>
|
||||
</FooterLink>
|
||||
</FooterSectionList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterSectionHeading(props: React.PropsWithChildren) {
|
||||
return <span className={'font-heading'}>{props.children}</span>;
|
||||
}
|
||||
|
||||
function FooterSectionList(props: React.PropsWithChildren) {
|
||||
return <ul className={'flex flex-col space-y-2.5'}>{props.children}</ul>;
|
||||
}
|
||||
|
||||
function FooterLink(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<li
|
||||
className={
|
||||
'text-sm text-muted-foreground hover:underline [&>a]:transition-colors'
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</li>
|
||||
sections={[
|
||||
{
|
||||
heading: <Trans i18nKey="marketing:about" />,
|
||||
links: [
|
||||
{ href: '/blog', label: <Trans i18nKey="marketing:blog" /> },
|
||||
{ href: '/contact', label: <Trans i18nKey="marketing:contact" /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: <Trans i18nKey="marketing:product" />,
|
||||
links: [
|
||||
{
|
||||
href: '/docs',
|
||||
label: <Trans i18nKey="marketing:documentation" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: <Trans i18nKey="marketing:legal" />,
|
||||
links: [
|
||||
{
|
||||
href: '/terms-of-service',
|
||||
label: <Trans i18nKey="marketing:termsOfService" />,
|
||||
},
|
||||
{
|
||||
href: '/privacy-policy',
|
||||
label: <Trans i18nKey="marketing:privacyPolicy" />,
|
||||
},
|
||||
{
|
||||
href: '/cookie-policy',
|
||||
label: <Trans i18nKey="marketing:cookiePolicy" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ function AuthButtons() {
|
||||
|
||||
<ArrowRightIcon
|
||||
className={
|
||||
'ml-1 h-4 w-4 transition-transform duration-500 group-hover:translate-x-1 hidden lg:block'
|
||||
'ml-1 hidden h-4 w-4 transition-transform duration-500 group-hover:translate-x-1 lg:block'
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { Header } from '@kit/ui/marketing';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
|
||||
import { SiteHeaderAccountSection } from './site-header-account-section';
|
||||
@@ -7,26 +9,10 @@ import { SiteNavigation } from './site-navigation';
|
||||
|
||||
export function SiteHeader(props: { user?: User | null }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'site-header sticky top-0 z-10 w-full bg-background/80 py-2 backdrop-blur-md dark:bg-background/50'
|
||||
}
|
||||
>
|
||||
<div className={'container'}>
|
||||
<div className="grid h-14 grid-cols-3 items-center">
|
||||
<div>
|
||||
<AppLogo />
|
||||
</div>
|
||||
|
||||
<div className={'order-first md:order-none'}>
|
||||
<SiteNavigation />
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center justify-end space-x-1'}>
|
||||
<SiteHeaderAccountSection user={props.user ?? null} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Header
|
||||
logo={<AppLogo />}
|
||||
navigation={<SiteNavigation />}
|
||||
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function PostHeader({ post }: { post: Cms.ContentItem }) {
|
||||
<div className={'mx-auto flex max-w-3xl flex-col space-y-4'}>
|
||||
<h1
|
||||
className={
|
||||
'font-heading text-3xl font-semibold dark:text-white xl:text-5xl tracking-tighter'
|
||||
'font-heading text-3xl font-semibold tracking-tighter dark:text-white xl:text-5xl'
|
||||
}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -4,16 +4,18 @@ import Link from 'next/link';
|
||||
import { ArrowRightIcon, LayoutDashboard } from 'lucide-react';
|
||||
|
||||
import { PricingTable } from '@kit/billing-gateway/marketing';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
CtaButton,
|
||||
FeatureCard,
|
||||
FeatureGrid,
|
||||
FeatureShowcase,
|
||||
FeatureShowcaseIconContainer,
|
||||
GradientSecondaryText,
|
||||
Hero,
|
||||
Pill,
|
||||
SecondaryHero,
|
||||
} from '@kit/ui/marketing';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
@@ -22,51 +24,26 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
function Home() {
|
||||
return (
|
||||
<div className={'mt-4 flex flex-col space-y-24 py-14'}>
|
||||
<div className={'container mx-auto flex flex-col space-y-20'}>
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-center md:flex-row' +
|
||||
' mx-auto flex-1 justify-center animate-in fade-in' +
|
||||
' duration-1000 zoom-in-90 slide-in-from-top-36'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex w-full flex-1 flex-col items-center space-y-6 xl:space-y-8 2xl:space-y-10'
|
||||
}
|
||||
>
|
||||
<Pill new>
|
||||
<Hero
|
||||
pill={
|
||||
<Pill label={'New'}>
|
||||
<span>The leading SaaS Starter Kit for ambitious developers</span>
|
||||
</Pill>
|
||||
|
||||
<div className={'flex flex-col items-center space-y-8'}>
|
||||
<HeroTitle>
|
||||
<span>The ultimate SaaS Starter</span>
|
||||
|
||||
<span>for your next project</span>
|
||||
</HeroTitle>
|
||||
|
||||
<div className={'flex max-w-2xl flex-col space-y-1'}>
|
||||
<Heading
|
||||
level={3}
|
||||
className={'p-0 text-center font-sans text-base font-normal'}
|
||||
>
|
||||
Build and Ship a SaaS faster than ever before with the
|
||||
next-gen SaaS Starter Kit. Ship your SaaS in days, not months.
|
||||
</Heading>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MainCallToActionButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
'mx-auto flex max-w-[100rem] justify-center py-8 animate-in fade-in ' +
|
||||
' delay-300 duration-1000 zoom-in-95 slide-in-from-top-32 fill-mode-both'
|
||||
}
|
||||
>
|
||||
title={
|
||||
<>
|
||||
<span>The ultimate SaaS Starter</span>
|
||||
<span>for your next project</span>
|
||||
</>
|
||||
}
|
||||
subtitle={
|
||||
<span>
|
||||
Build and Ship a SaaS faster than ever before with the next-gen SaaS
|
||||
Starter Kit. Ship your SaaS in days, not months.
|
||||
</span>
|
||||
}
|
||||
cta={<MainCallToActionButton />}
|
||||
image={
|
||||
<Image
|
||||
priority
|
||||
className={
|
||||
@@ -77,55 +54,39 @@ function Home() {
|
||||
src={`/images/dashboard.webp`}
|
||||
alt={`App Image`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<div
|
||||
className={'flex flex-col space-y-16 xl:space-y-32 2xl:space-y-36'}
|
||||
>
|
||||
<FeatureShowcaseContainer>
|
||||
<FeatureContainer className={'w-full max-w-5xl'}>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<IconContainer>
|
||||
<LayoutDashboard className={'h-5'} />
|
||||
|
||||
<span>SaaS Starter Kit</span>
|
||||
</IconContainer>
|
||||
|
||||
<div className={'flex flex-col'}>
|
||||
<h3
|
||||
className={
|
||||
'text-3xl font-normal tracking-tighter xl:text-5xl'
|
||||
}
|
||||
>
|
||||
<b className={'font-semibold dark:text-white'}>
|
||||
<FeatureShowcase
|
||||
heading={
|
||||
<>
|
||||
<b className="font-semibold dark:text-white">
|
||||
The ultimate SaaS Starter Kit
|
||||
</b>
|
||||
.{' '}
|
||||
<GradientSecondaryText
|
||||
className={
|
||||
'from-foreground/70 to-foreground/80 font-medium'
|
||||
}
|
||||
>
|
||||
Unleash your creativity and build your SaaS faster than
|
||||
ever with Makerkit.
|
||||
<GradientSecondaryText>
|
||||
Unleash your creativity and build your SaaS faster than ever
|
||||
with Makerkit.
|
||||
</GradientSecondaryText>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</FeatureContainer>
|
||||
|
||||
<FeatureContainer
|
||||
className={
|
||||
'grid w-full grid-cols-1 gap-6 space-y-0 lg:grid-cols-3'
|
||||
</>
|
||||
}
|
||||
icon={
|
||||
<FeatureShowcaseIconContainer>
|
||||
<LayoutDashboard className="h-5" />
|
||||
<span>All-in-one solution</span>
|
||||
</FeatureShowcaseIconContainer>
|
||||
}
|
||||
>
|
||||
<FeatureGrid>
|
||||
<FeatureCard
|
||||
className={
|
||||
'relative col-span-2 overflow-hidden bg-violet-500 text-white lg:h-96'
|
||||
}
|
||||
title={'Beautiful Dashboard'}
|
||||
label={'Beautiful Dashboard'}
|
||||
description={`Makerkit provides a beautiful dashboard to manage your SaaS business.`}
|
||||
>
|
||||
<Image
|
||||
@@ -141,7 +102,7 @@ function Home() {
|
||||
className={
|
||||
'relative col-span-2 w-full overflow-hidden lg:col-span-1'
|
||||
}
|
||||
title={'Authentication'}
|
||||
label={'Authentication'}
|
||||
description={`Makerkit provides a variety of providers to allow your users to sign in.`}
|
||||
>
|
||||
<Image
|
||||
@@ -157,7 +118,7 @@ function Home() {
|
||||
className={
|
||||
'relative col-span-2 overflow-hidden lg:col-span-1 lg:h-96'
|
||||
}
|
||||
title={'Multi Tenancy'}
|
||||
label={'Multi Tenancy'}
|
||||
description={`Multi tenant memberships for your SaaS business.`}
|
||||
>
|
||||
<Image
|
||||
@@ -171,7 +132,7 @@ function Home() {
|
||||
|
||||
<FeatureCard
|
||||
className={'relative col-span-2 overflow-hidden lg:h-96'}
|
||||
title={'Billing'}
|
||||
label={'Billing'}
|
||||
description={`Makerkit supports multiple payment gateways to charge your customers.`}
|
||||
>
|
||||
<Image
|
||||
@@ -182,8 +143,8 @@ function Home() {
|
||||
alt={'Billing'}
|
||||
/>
|
||||
</FeatureCard>
|
||||
</FeatureContainer>
|
||||
</FeatureShowcaseContainer>
|
||||
</FeatureGrid>
|
||||
</FeatureShowcase>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -193,24 +154,11 @@ function Home() {
|
||||
'flex flex-col items-center justify-center space-y-16 py-16'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-col items-center space-y-4 text-center'}>
|
||||
<Pill>Get started for free. No credit card required.</Pill>
|
||||
|
||||
<div className={'flex flex-col'}>
|
||||
<Heading level={2} className={'tracking-tighter'}>
|
||||
Fair pricing for all types of businesses
|
||||
</Heading>
|
||||
|
||||
<Heading
|
||||
level={3}
|
||||
className={
|
||||
'font-sans font-normal tracking-tight text-muted-foreground'
|
||||
}
|
||||
>
|
||||
Get started on our free plan and upgrade when you are ready.
|
||||
</Heading>
|
||||
</div>
|
||||
</div>
|
||||
<SecondaryHero
|
||||
pill={<Pill>Get started for free. No credit card required.</Pill>}
|
||||
heading="Fair pricing for all types of businesses"
|
||||
subheading="Get started on our free plan and upgrade when you are ready."
|
||||
/>
|
||||
|
||||
<div className={'w-full'}>
|
||||
<PricingTable
|
||||
@@ -229,81 +177,10 @@ function Home() {
|
||||
|
||||
export default withI18n(Home);
|
||||
|
||||
function HeroTitle({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<h1
|
||||
className={
|
||||
'hero-title flex flex-col space-y-1 text-center font-sans text-4xl font-semibold tracking-tighter dark:text-white sm:text-6xl lg:max-w-5xl lg:text-7xl xl:text-[5.125rem]'
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
function Pill(
|
||||
props: React.PropsWithChildren<{
|
||||
new?: boolean;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<h2
|
||||
className={
|
||||
'space-x-2.5 rounded-full border border-gray-100 px-2 py-2.5 text-center text-sm font-medium text-transparent dark:border-primary/10'
|
||||
}
|
||||
>
|
||||
{props.new && (
|
||||
<span
|
||||
className={
|
||||
'rounded-2xl bg-primary px-2.5 py-1.5 text-sm font-semibold text-primary-foreground'
|
||||
}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
<GradientSecondaryText>{props.children}</GradientSecondaryText>
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureShowcaseContainer(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className={'flex flex-col justify-between space-y-8'}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureContainer(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
reverse?: boolean;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full flex-col space-y-6 py-4',
|
||||
{
|
||||
'order-2 mt-8 lg:order-none lg:mt-0': props.reverse,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MainCallToActionButton() {
|
||||
return (
|
||||
<div className={'flex space-x-4'}>
|
||||
<Button
|
||||
className={
|
||||
'h-12 rounded-xl px-4 text-base font-semibold transition-all hover:shadow-2xl dark:shadow-primary/30'
|
||||
}
|
||||
asChild
|
||||
>
|
||||
<CtaButton>
|
||||
<Link href={'/auth/sign-up'}>
|
||||
<span className={'flex items-center space-x-0.5'}>
|
||||
<span>
|
||||
@@ -318,84 +195,13 @@ function MainCallToActionButton() {
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CtaButton>
|
||||
|
||||
<Button
|
||||
variant={'link'}
|
||||
className={'h-12 rounded-xl px-4 text-base font-semibold'}
|
||||
asChild
|
||||
>
|
||||
<CtaButton variant={'link'}>
|
||||
<Link href={'/contact'}>
|
||||
<Trans i18nKey={'common:contactUs'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</CtaButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconContainer(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex'}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center space-x-4 rounded-lg p-3 font-semibold',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureCard(
|
||||
props: React.PropsWithChildren<{
|
||||
title: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
props.className,
|
||||
'rounded-3xl p-2 ring-2 ring-gray-100 dark:ring-primary/10',
|
||||
)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className={'text-xl font-semibold'}>{props.title}</CardTitle>
|
||||
|
||||
<CardDescription
|
||||
className={
|
||||
'max-w-xs text-sm font-semibold tracking-tight text-current'
|
||||
}
|
||||
>
|
||||
{props.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>{props.children}</CardContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GradientSecondaryText(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'bg-gradient-to-r from-foreground/60 to-foreground bg-clip-text text-transparent',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
10
packages/analytics/README.md
Normal file
10
packages/analytics/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Analytics - @kit/analytics
|
||||
|
||||
@kit/analytics Package provides a simple and consistent API for tracking analytics events in web applications.
|
||||
|
||||
## Overview
|
||||
|
||||
This version of the `@kit/analytics` package uses classes internally for structure and encapsulation, but exposes a functional API for ease of use and consistency with Makerkit's style.
|
||||
|
||||
## Implementation
|
||||
|
||||
35
packages/analytics/package.json
Normal file
35
packages/analytics/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@kit/analytics",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tailwind-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@kit/eslint-config/base",
|
||||
"@kit/eslint-config/react"
|
||||
]
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
72
packages/analytics/src/analytics-manager.ts
Normal file
72
packages/analytics/src/analytics-manager.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NullAnalyticsService } from './null-analytics-service';
|
||||
import {
|
||||
AnalyticsManager,
|
||||
AnalyticsService,
|
||||
CreateAnalyticsManagerOptions,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Creates an analytics manager that can be used to track page views and events. The manager is initialized with a
|
||||
* default provider and can be switched to a different provider at any time. The manager will use a NullAnalyticsService
|
||||
* if the provider is not registered.
|
||||
* @param options
|
||||
*/
|
||||
export function createAnalyticsManager<T extends string, Config extends object>(
|
||||
options: CreateAnalyticsManagerOptions<T, Config>,
|
||||
): AnalyticsManager {
|
||||
let activeService: AnalyticsService = NullAnalyticsService;
|
||||
|
||||
const getActiveService = (): AnalyticsService => {
|
||||
if (activeService === NullAnalyticsService) {
|
||||
console.warn(
|
||||
'Analytics service not initialized. Using NullAnalyticsService.',
|
||||
);
|
||||
}
|
||||
|
||||
return activeService;
|
||||
};
|
||||
|
||||
const initialize = (provider: T, config: Config) => {
|
||||
const factory = options.providers[provider];
|
||||
|
||||
if (!factory) {
|
||||
console.error(
|
||||
`Analytics provider '${provider}' not registered. Using NullAnalyticsService.`,
|
||||
);
|
||||
|
||||
activeService = NullAnalyticsService;
|
||||
return;
|
||||
}
|
||||
|
||||
activeService = factory(config);
|
||||
activeService.initialize();
|
||||
};
|
||||
|
||||
// Initialize with the default provider
|
||||
initialize(options.defaultProvider, {} as Config);
|
||||
|
||||
return {
|
||||
identify: (userId: string, traits?: Record<string, string>) => {
|
||||
return getActiveService().identify(userId, traits);
|
||||
},
|
||||
|
||||
/**
|
||||
* Track a page view with the given URL.
|
||||
* @param url
|
||||
*/
|
||||
trackPageView: (url: string) => {
|
||||
return getActiveService().trackPageView(url);
|
||||
},
|
||||
/**
|
||||
* Track an event with the given name and properties.
|
||||
* @param eventName
|
||||
* @param eventProperties
|
||||
*/
|
||||
trackEvent: (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string | string[]>,
|
||||
) => {
|
||||
return getActiveService().trackEvent(eventName, eventProperties);
|
||||
},
|
||||
};
|
||||
}
|
||||
10
packages/analytics/src/index.ts
Normal file
10
packages/analytics/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createAnalyticsManager } from './analytics-manager';
|
||||
import { NullAnalyticsService } from './null-analytics-service';
|
||||
import type { AnalyticsManager } from './types';
|
||||
|
||||
export const analytics: AnalyticsManager = createAnalyticsManager({
|
||||
defaultProvider: 'null',
|
||||
providers: {
|
||||
null: () => NullAnalyticsService,
|
||||
},
|
||||
});
|
||||
16
packages/analytics/src/null-analytics-service.ts
Normal file
16
packages/analytics/src/null-analytics-service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AnalyticsService } from './types';
|
||||
|
||||
const noop = () => {
|
||||
// do nothing - this is to prevent errors when the analytics service is not initialized
|
||||
};
|
||||
|
||||
/**
|
||||
* Null analytics service that does nothing. It is initialized with a noop function. This is useful for testing or when
|
||||
* the user is calling analytics methods before the analytics service is initialized.
|
||||
*/
|
||||
export const NullAnalyticsService: AnalyticsService = {
|
||||
initialize: noop,
|
||||
trackPageView: noop,
|
||||
trackEvent: noop,
|
||||
identify: noop,
|
||||
};
|
||||
32
packages/analytics/src/types.ts
Normal file
32
packages/analytics/src/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
interface TrackEvent {
|
||||
trackEvent(
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string | string[]>,
|
||||
): void;
|
||||
}
|
||||
|
||||
interface TrackPageView {
|
||||
trackPageView(url: string): void;
|
||||
}
|
||||
|
||||
interface Identify {
|
||||
identify(userId: string, traits?: Record<string, string>): void;
|
||||
}
|
||||
|
||||
export interface AnalyticsService extends TrackPageView, TrackEvent, Identify {
|
||||
initialize(): void;
|
||||
}
|
||||
|
||||
export type AnalyticsProviderFactory<Config extends object> = (
|
||||
config: Config,
|
||||
) => AnalyticsService;
|
||||
|
||||
export interface CreateAnalyticsManagerOptions<
|
||||
T extends string,
|
||||
Config extends object,
|
||||
> {
|
||||
defaultProvider: T;
|
||||
providers: Record<T, AnalyticsProviderFactory<Config>>;
|
||||
}
|
||||
|
||||
export interface AnalyticsManager extends TrackPageView, TrackEvent, Identify {}
|
||||
8
packages/analytics/tsconfig.json
Normal file
8
packages/analytics/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -189,7 +189,7 @@ function PricingItem(
|
||||
<div className={'flex items-center space-x-6'}>
|
||||
<b
|
||||
className={
|
||||
'text-current-foreground font-heading tracking-tight font-semibold uppercase'
|
||||
'text-current-foreground font-heading font-semibold uppercase tracking-tight'
|
||||
}
|
||||
>
|
||||
<Trans
|
||||
@@ -341,7 +341,7 @@ function Price({ children }: React.PropsWithChildren) {
|
||||
>
|
||||
<span
|
||||
className={
|
||||
'font-heading flex items-center text-3xl font-bold lg:text-4xl tracking-tighter'
|
||||
'font-heading flex items-center text-3xl font-bold tracking-tighter lg:text-4xl'
|
||||
}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"./cookie-banner": "./src/makerkit/cookie-banner.tsx",
|
||||
"./card-button": "./src/makerkit/card-button.tsx",
|
||||
"./version-updater": "./src/makerkit/version-updater.tsx",
|
||||
"./multi-step-form": "./src/makerkit/multi-step-form.tsx"
|
||||
"./multi-step-form": "./src/makerkit/multi-step-form.tsx",
|
||||
"./marketing": "./src/makerkit/marketing/index.tsx"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
23
packages/ui/src/makerkit/marketing/cta-button.tsx
Normal file
23
packages/ui/src/makerkit/marketing/cta-button.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Button } from '../../shadcn/button';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
export const CtaButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(function CtaButtonComponent({ className, children, ...props }, ref) {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'h-12 rounded-xl px-4 text-base font-semibold transition-all hover:shadow-2xl dark:shadow-primary/30',
|
||||
className,
|
||||
)}
|
||||
asChild
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
44
packages/ui/src/makerkit/marketing/feature-card.tsx
Normal file
44
packages/ui/src/makerkit/marketing/feature-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import {
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '../../shadcn/card';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface FeatureCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
label: string;
|
||||
description: string;
|
||||
image?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FeatureCard = forwardRef<HTMLDivElement, FeatureCardProps>(
|
||||
function FeatureCardComponent(
|
||||
{ className, label, description, image, children, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-3xl p-2 ring-2 ring-gray-100 dark:ring-primary/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-semibold">{label}</CardTitle>
|
||||
<CardDescription className="max-w-xs text-sm font-semibold tracking-tight text-current">
|
||||
{description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{image}
|
||||
{children}
|
||||
</CardContent>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
21
packages/ui/src/makerkit/marketing/feature-grid.tsx
Normal file
21
packages/ui/src/makerkit/marketing/feature-grid.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
export const FeatureGrid = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(function FeatureGridComponent({ className, children, ...props }, ref) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'grid w-full grid-cols-1 gap-6 space-y-0 lg:grid-cols-3',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
50
packages/ui/src/makerkit/marketing/feature-showcase.tsx
Normal file
50
packages/ui/src/makerkit/marketing/feature-showcase.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface FeatureShowcaseProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
heading: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FeatureShowcase = forwardRef<HTMLDivElement, FeatureShowcaseProps>(
|
||||
function FeatureShowcaseComponent(
|
||||
{ className, heading, icon, children, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col justify-between space-y-8', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex w-full max-w-5xl flex-col space-y-4">
|
||||
{icon && <div className="flex">{icon}</div>}
|
||||
<h3 className="text-3xl font-normal tracking-tighter xl:text-5xl">
|
||||
{heading}
|
||||
</h3>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export function FeatureShowcaseIconContainer(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex'}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center space-x-4 rounded-lg p-3 font-semibold',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
packages/ui/src/makerkit/marketing/footer.tsx
Normal file
95
packages/ui/src/makerkit/marketing/footer.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface FooterSection {
|
||||
heading: React.ReactNode;
|
||||
links: Array<{
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface FooterProps extends React.HTMLAttributes<HTMLElement> {
|
||||
logo: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
copyright: React.ReactNode;
|
||||
sections: FooterSection[];
|
||||
}
|
||||
|
||||
export const Footer = forwardRef<HTMLElement, FooterProps>(
|
||||
function MarketingFooterComponent(
|
||||
{ className, logo, description, copyright, sections, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<footer
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'site-footer relative mt-auto w-full py-8 2xl:py-16',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-y-0">
|
||||
<div className="flex w-full space-x-2 lg:w-4/12 xl:w-4/12 xl:space-x-6 2xl:space-x-8">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>{logo}</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex text-xs text-muted-foreground">
|
||||
<p>{copyright}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col space-y-8 lg:flex-row lg:justify-end lg:space-x-6 lg:space-y-0 xl:space-x-16">
|
||||
{sections.map((section, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex flex-col space-y-2.5">
|
||||
<FooterSectionHeading>
|
||||
{section.heading}
|
||||
</FooterSectionHeading>
|
||||
|
||||
<FooterSectionList>
|
||||
{section.links.map((link, linkIndex) => (
|
||||
<FooterLink key={linkIndex} href={link.href}>
|
||||
{link.label}
|
||||
</FooterLink>
|
||||
))}
|
||||
</FooterSectionList>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function FooterSectionHeading(props: React.PropsWithChildren) {
|
||||
return <span className="font-heading">{props.children}</span>;
|
||||
}
|
||||
|
||||
function FooterSectionList(props: React.PropsWithChildren) {
|
||||
return <ul className="flex flex-col space-y-2.5">{props.children}</ul>;
|
||||
}
|
||||
|
||||
function FooterLink({
|
||||
href,
|
||||
children,
|
||||
}: React.PropsWithChildren<{ href: string }>) {
|
||||
return (
|
||||
<li className="text-sm text-muted-foreground hover:underline [&>a]:transition-colors">
|
||||
<a href={href}>{children}</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
export const GradientSecondaryText = forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.HTMLAttributes<HTMLSpanElement> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(function GradientSecondaryTextComponent({ className, ...props }, ref) {
|
||||
const Comp = props.asChild ? Slot : 'span';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-gradient-to-r from-foreground/50 to-foreground bg-clip-text text-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Slottable>{props.children}</Slottable>
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
21
packages/ui/src/makerkit/marketing/gradient-text.tsx
Normal file
21
packages/ui/src/makerkit/marketing/gradient-text.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
export const GradientText = forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.HTMLAttributes<HTMLSpanElement>
|
||||
>(function GradientTextComponent({ className, children, ...props }, ref) {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-gradient-to-r bg-clip-text text-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
37
packages/ui/src/makerkit/marketing/header.tsx
Normal file
37
packages/ui/src/makerkit/marketing/header.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
logo?: React.ReactNode;
|
||||
navigation?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Header = forwardRef<HTMLDivElement, HeaderProps>(
|
||||
function MarketingHeaderComponent(
|
||||
{ className, logo, navigation, actions, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'site-header sticky top-0 z-10 w-full bg-background/80 py-2 backdrop-blur-md dark:bg-background/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="grid h-14 grid-cols-3 items-center">
|
||||
<div>{logo}</div>
|
||||
<div className="order-first md:order-none">{navigation}</div>
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
27
packages/ui/src/makerkit/marketing/hero-title.tsx
Normal file
27
packages/ui/src/makerkit/marketing/hero-title.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
export const HeroTitle = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(function HeroTitleComponent({ children, className, ...props }, ref) {
|
||||
const Comp = props.asChild ? Slot : 'h1';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'hero-title flex flex-col space-y-1 text-center font-sans text-4xl font-semibold tracking-tighter dark:text-white sm:text-6xl lg:max-w-5xl lg:text-7xl xl:text-[5.125rem]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Slottable>{children}</Slottable>
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
89
packages/ui/src/makerkit/marketing/hero.tsx
Normal file
89
packages/ui/src/makerkit/marketing/hero.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Heading } from '../../shadcn/heading';
|
||||
import { cn } from '../../utils';
|
||||
import { HeroTitle } from './hero-title';
|
||||
|
||||
interface HeroProps {
|
||||
pill?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
cta?: React.ReactNode;
|
||||
image?: React.ReactNode;
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export function Hero({
|
||||
pill,
|
||||
title,
|
||||
subtitle,
|
||||
cta,
|
||||
image,
|
||||
className,
|
||||
animate = true,
|
||||
}: HeroProps) {
|
||||
return (
|
||||
<div className={cn('mx-auto flex flex-col space-y-20', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto flex flex-1 flex-col items-center justify-center md:flex-row',
|
||||
{
|
||||
['duration-1000 animate-in fade-in zoom-in-90 slide-in-from-top-36']:
|
||||
animate,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-1 flex-col items-center space-y-6 xl:space-y-8 2xl:space-y-10">
|
||||
{pill && (
|
||||
<div
|
||||
className={cn({
|
||||
['delay-300 duration-700 animate-in fade-in fill-mode-both']:
|
||||
animate,
|
||||
})}
|
||||
>
|
||||
{pill}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center space-y-8">
|
||||
<HeroTitle>{title}</HeroTitle>
|
||||
|
||||
{subtitle && (
|
||||
<div className="flex max-w-2xl flex-col space-y-1">
|
||||
<Heading
|
||||
level={3}
|
||||
className="p-0 text-center font-sans text-base font-normal"
|
||||
>
|
||||
{subtitle}
|
||||
</Heading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cta && (
|
||||
<div
|
||||
className={cn({
|
||||
['delay-500 duration-1000 animate-in fade-in fill-mode-both']:
|
||||
animate,
|
||||
})}
|
||||
>
|
||||
{cta}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{image && (
|
||||
<div
|
||||
className={cn('mx-auto flex max-w-[85rem] justify-center py-8', {
|
||||
['delay-300 duration-1000 animate-in fade-in zoom-in-95 slide-in-from-top-32 fill-mode-both']:
|
||||
animate,
|
||||
})}
|
||||
>
|
||||
{image}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
packages/ui/src/makerkit/marketing/index.tsx
Normal file
12
packages/ui/src/makerkit/marketing/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './hero-title';
|
||||
export * from './pill';
|
||||
export * from './gradient-secondary-text';
|
||||
export * from './gradient-text';
|
||||
export * from './hero';
|
||||
export * from './secondary-hero';
|
||||
export * from './cta-button';
|
||||
export * from './header';
|
||||
export * from './footer';
|
||||
export * from './feature-showcase';
|
||||
export * from './feature-grid';
|
||||
export * from './feature-card';
|
||||
40
packages/ui/src/makerkit/marketing/pill.tsx
Normal file
40
packages/ui/src/makerkit/marketing/pill.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
import { GradientSecondaryText } from './gradient-secondary-text';
|
||||
|
||||
export const Pill = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
label?: string;
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(function PillComponent({ className, asChild, ...props }, ref) {
|
||||
const Comp = asChild ? Slot : 'h3';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'space-x-2.5 rounded-full border border-gray-100 px-2 py-2.5 text-center text-sm font-medium text-transparent dark:border-primary/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{props.label && (
|
||||
<span
|
||||
className={
|
||||
'rounded-2xl bg-primary px-2.5 py-1.5 text-sm font-semibold text-primary-foreground'
|
||||
}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
)}
|
||||
<Slottable>
|
||||
<GradientSecondaryText>{props.children}</GradientSecondaryText>
|
||||
</Slottable>
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
49
packages/ui/src/makerkit/marketing/secondary-hero.tsx
Normal file
49
packages/ui/src/makerkit/marketing/secondary-hero.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Heading } from '../../shadcn/heading';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface SecondaryHeroProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
pill?: React.ReactNode;
|
||||
heading: React.ReactNode;
|
||||
subheading: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SecondaryHero = forwardRef<HTMLDivElement, SecondaryHeroProps>(
|
||||
function SecondaryHeroComponent(
|
||||
{ className, pill, heading, subheading, children, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-col items-center space-y-4 text-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{pill && (
|
||||
<div className="delay-300 duration-700 animate-in fade-in">
|
||||
{pill}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Heading level={2} className="tracking-tighter">
|
||||
{heading}
|
||||
</Heading>
|
||||
|
||||
<Heading
|
||||
level={3}
|
||||
className="font-sans font-normal tracking-tight text-muted-foreground"
|
||||
>
|
||||
{subheading}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -218,6 +218,21 @@ importers:
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
|
||||
packages/analytics:
|
||||
devDependencies:
|
||||
'@kit/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../tooling/eslint
|
||||
'@kit/prettier-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../tooling/prettier
|
||||
'@kit/tailwind-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../tooling/tailwind
|
||||
'@kit/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../../tooling/typescript
|
||||
|
||||
packages/billing/core:
|
||||
devDependencies:
|
||||
'@kit/eslint-config':
|
||||
|
||||
Reference in New Issue
Block a user