Skip to content

Commit

Permalink
Merge pull request #23 from saas-kits/feat/pricing-refactoring
Browse files Browse the repository at this point in the history
Feat/pricing refactoring
  • Loading branch information
shyamlohar authored Dec 27, 2023
2 parents f651b57 + e4b48f6 commit 82ef732
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 293 deletions.
53 changes: 53 additions & 0 deletions app/components/pricing/containers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { cn } from "@/lib/utils"

type PricingCardProps = {
children: React.ReactNode
isFeatured?: boolean
}
export const PricingCard = ({ children, isFeatured }: PricingCardProps) => {
return (
<div
className={cn("rounded-[13px] p-px", {
"bg-gradient-to-b from-zinc-400 to-white dark:from-zinc-500 dark:to-black":
isFeatured,
"bg-gradient-to-b from-border to-white dark:to-black": !isFeatured,
})}
>
<div className="relative h-full w-full rounded-xl bg-white p-6 pb-24 dark:bg-background">
{children}
</div>
</div>
)
}

type FeatureListContainerProps = {
children: React.ReactNode
}

export const FeatureListContainer = ({
children,
}: FeatureListContainerProps) => {
return (
<ul className="mt-8 space-y-3 text-sm leading-6 xl:mt-10">{children}</ul>
)
}

export const CTAContainer = ({ children }: FeatureListContainerProps) => {
return (
<div className="absolute bottom-0 left-0 mx-6 mb-6 mt-8 w-[calc(100%-48px)]">
{children}
</div>
)
}

type FeaturedBadgeContainerProps = {
children: React.ReactNode
}

export const FeaturedBadgeContainer = ({children}: FeaturedBadgeContainerProps) => {
return (
<div className="absolute -top-2 left-0 flex h-4 w-full items-center justify-center text-sm">
<span className="rounded-full bg-black px-4 py-1 text-xs font-semibold text-white dark:bg-white dark:text-black">{children}</span>
</div>
)
}
70 changes: 70 additions & 0 deletions app/components/pricing/feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
import clsx from "clsx"

export type FeatureType = {
name: string
isAvailable: boolean
inProgress: boolean
}

export const Feature = ({ name, isAvailable, inProgress }: FeatureType) => (
<li
className={clsx(
inProgress && "text-muted",
"flex gap-x-3 text-muted-foreground"
)}
>
{/* If in progress return disabled */}
{!isAvailable ? (
<Cross2Icon className={"h-6 w-5 flex-none"} aria-hidden="true" />
) : (
<CheckIcon className={"h-6 w-5 flex-none"} aria-hidden="true" />
)}
{name}{" "}
{inProgress && (
<span className="text-xs font-semibold leading-6 text-muted-foreground">
(Coming Soon)
</span>
)}
</li>
)

type FeatureTitleProps = {
children: React.ReactNode
}

export const FeatureTitle = ({ children }: FeatureTitleProps) => {
return <div className="text-base font-semibold leading-8">{children}</div>
}

type FeatureDescriptionProps = {
children: React.ReactNode
}

export const FeatureDescription = ({ children }: FeatureDescriptionProps) => {
return (
<p className="wrap-balance mt-4 text-sm font-light leading-5 text-muted-foreground">
{children}
</p>
)
}

type FeaturePriceProps = {
interval: string
price: string
}

export const FeaturePrice = ({
interval,
price,
}: FeaturePriceProps) => {
return (
<h4 className="mt-6 text-4xl font-bold tracking-tight">
{price}
<span className="text-sm font-semibold leading-6 text-muted-foreground">
/{interval}
</span>
</h4>
)
}
14 changes: 14 additions & 0 deletions app/components/pricing/pricing-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"

type PricingSwitchProps = {
onSwitch: (value: string) => void
}

export const PricingSwitch = ({ onSwitch }: PricingSwitchProps) => (
<Tabs defaultValue="0" className="mx-auto w-40" onValueChange={onSwitch}>
<TabsList>
<TabsTrigger value="0">Monthly</TabsTrigger>
<TabsTrigger value="1">Yearly</TabsTrigger>
</TabsList>
</Tabs>
)
17 changes: 2 additions & 15 deletions app/lib/server/seo/seo-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,8 @@ import type { MetaDescriptor, MetaFunction } from "@remix-run/node"

export const getDefaultSeoTags = (baseUrl: string) => {
return {
title: "Root Layout tags",
description: "Root Layout tags",
openGraph: {
url: `${baseUrl}/robots.txt`,
title: "Robots.txt",
description: "Robots.txt",
images: [
{
url: `${baseUrl}/static/images/seo/seo.png`,
alt: "Robots.txt",
height: 630,
width: 1200,
},
],
},
title: "Remix SaaSkit",
description: "Remix SaaSkit description placeholder",
}
}

Expand Down
2 changes: 0 additions & 2 deletions app/lib/server/sitemap/sitemap-utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import isEqual from "lodash-es/isEqual.js"

import type { SEOHandle, SitemapEntry } from "./types.ts"

console.log({ isEqual })

type Options = {
siteUrl: string
}
Expand Down
11 changes: 11 additions & 0 deletions app/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { CURRENCIES } from "@/services/stripe/plans.config"
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

export const getformattedCurrency = (
amount: number,
defaultCurrency: CURRENCIES
) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: defaultCurrency,
}).format(amount)
}
1 change: 0 additions & 1 deletion app/routes/_auth+/auth.logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { authenticator } from "@/services/auth.server"
import type { ActionFunctionArgs } from "@remix-run/node"

export async function action({ request }: ActionFunctionArgs) {
console.log("called")
await authenticator.logout(request, { redirectTo: "/login" })
}
2 changes: 1 addition & 1 deletion app/routes/_auth+/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const schema = z.object({
export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to /dashboard directly
return await authenticator.isAuthenticated(request, {
successRedirect: "/",
successRedirect: "/dashboard",
})
}

Expand Down
2 changes: 1 addition & 1 deletion app/routes/_auth+/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const schema = z
export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to /dashboard directly
return await authenticator.isAuthenticated(request, {
successRedirect: "/",
successRedirect: "/dashboard",
})
}

Expand Down
2 changes: 1 addition & 1 deletion app/routes/_auth+/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { Label } from "@/components/ui/label"
export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to /dashboard directly
return await authenticator.isAuthenticated(request, {
successRedirect: "/",
successRedirect: "/dashboard",
})
}

Expand Down
4 changes: 4 additions & 0 deletions app/routes/_auth+/verify-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
const user = await authenticator.isAuthenticated(request)

if (user) {
if (user.emailVerified) {
return redirect("/dashboard")
}

const result = await prisma.verificationCode.findFirst({
where: {
userId: user.id,
Expand Down
92 changes: 92 additions & 0 deletions app/routes/_index/pricing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useState } from "react"
import { NavLink, useLoaderData } from "@remix-run/react"

import { getformattedCurrency } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
CTAContainer,
FeaturedBadgeContainer,
FeatureListContainer,
PricingCard,
} from "@/components/pricing/containers"
import {
Feature,
FeatureDescription,
FeaturePrice,
FeatureTitle,
FeatureType,
} from "@/components/pricing/feature"
import { PricingSwitch } from "@/components/pricing/pricing-switch"

import type { loader } from "./route"

export const Pricing = () => {
const { plans, defaultCurrency } = useLoaderData<typeof loader>()
const [interval, setInterval] = useState<"month" | "year">("month")

return (
<div>
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-4xl text-center">
<h1 className="wrap-balance mt-16 bg-black bg-gradient-to-br bg-clip-text text-center text-4xl font-medium leading-tight text-transparent dark:from-white dark:to-[hsla(0,0%,100%,.5)] sm:text-5xl sm:leading-tight">
Pricing Plans
</h1>
</div>
<p className="wrap-balance mt-6 text-center text-lg font-light leading-7 text-muted-foreground">
{/* TODO: add content here @keyur */}
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Rerum
quisquam, iusto voluptatem dolore voluptas non laboriosam soluta quos
quod eos! Sapiente archit
</p>
<div className="mt-16 flex justify-center"></div>
<PricingSwitch
onSwitch={(value) => setInterval(value === "0" ? "month" : "year")}
/>

<div className="isolate mx-auto mt-10 grid max-w-md grid-cols-1 gap-8 lg:max-w-5xl lg:grid-cols-3">
{plans.map((plan) => {
const discount = plan.prices[0].amount * 12 - plan.prices[1].amount
const showDiscount =
interval === "year" && plan.prices[0].amount !== 0
const planPrice = plan.prices.find(
(p) => p.currency === defaultCurrency && p.interval == interval
)?.amount as number

return (
<PricingCard key={plan.id} isFeatured={showDiscount}>
{showDiscount && discount > 0 && (
<FeaturedBadgeContainer>
Save {getformattedCurrency(discount, defaultCurrency)}
</FeaturedBadgeContainer>
)}
<FeatureTitle>{plan.name}</FeatureTitle>
<FeatureDescription>{plan.description}</FeatureDescription>
<FeaturePrice
interval={interval}
price={getformattedCurrency(planPrice, defaultCurrency)}
/>
<FeatureListContainer>
{(plan.listOfFeatures as FeatureType[]).map(
(feature, index) => (
<Feature
key={index}
name={feature.name}
isAvailable={feature.isAvailable}
inProgress={feature.inProgress}
/>
)
)}
</FeatureListContainer>
<CTAContainer>
<NavLink to="/login">
<Button className="w-full">Choose Plan</Button>
</NavLink>
</CTAContainer>
</PricingCard>
)
})}
</div>
</div>
</div>
)
}
35 changes: 32 additions & 3 deletions app/routes/_index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
} from "@remix-run/node"

import { mergeMeta } from "@/lib/server/seo/seo-helpers"
import buildTags from "@/lib/server/seo/seo-utils"

import Faqs from "./faq"
import { FeatureSection } from "./feature-section"
Expand All @@ -14,15 +13,44 @@ import FeaturesVariantB from "./features-variant-b"
import Footer from "./footer"
import { HeroSection } from "./hero-section"
import { LogoCloud } from "./logo-cloud"
import { getAllPlans } from "@/models/plan"
import { getUserCurrencyFromRequest } from "@/utils/currency"
import { authenticator } from "@/services/auth.server"
import { Pricing } from "./pricing"

const loginFeatures = [
"Lorem ipsum, dolor sit amet consectetur adipisicing elit aute id magna.",
"Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo.",
"Ac tincidunt sapien vehicula erat auctor pellentesque rhoncus.",
]

export const loader = ({}: LoaderFunctionArgs) => {
return json({ hostUrl: process.env.HOST_URL })
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
})

let plans = await getAllPlans()

const defaultCurrency = getUserCurrencyFromRequest(request)

plans = plans
.map((plan) => {
return {
...plan,
prices: plan.prices
.filter((price) => price.currency === defaultCurrency)
.map((price) => ({
...price,
amount: price.amount / 100,
})),
}
})
.sort((a, b) => a.prices[0].amount - b.prices[0].amount)

return {
plans,
defaultCurrency,
}
}

export const meta: MetaFunction = mergeMeta(
Expand All @@ -46,6 +74,7 @@ export default function Index() {
lightFeatureImage="/login-light.jpeg"
/>
<FeaturesVariantB />
<Pricing/>
<Faqs />
<Footer />
</div>
Expand Down
Loading

0 comments on commit 82ef732

Please sign in to comment.