-
Notifications
You must be signed in to change notification settings - Fork 135
Open
Description
Set Of Files To Replace Price Card Metadata With Six Features You Can Enter On the stripe-fixtures File
I thought to provide these as a service to other users after I finangled the AI into handling the job correctly:
product-metadata.ts
// @ts-nocheck
import z from 'zod';
export const priceCardVariantSchema = z.enum(['basic', 'pro', 'enterprise']);
// Legacy metadata schema for backward compatibility
const legacyMetadataSchema = z.object({
price_card_variant: priceCardVariantSchema,
generated_images: z.string().optional(),
image_editor: z.enum(['basic', 'pro']),
support_level: z.enum(['email', 'live']),
});
// New metadata schema
const newMetadataSchema = z.object({
price_card_variant: priceCardVariantSchema,
feature_one: z.string(),
feature_two: z.string(),
feature_three: z.string(),
feature_four: z.string(),
feature_five: z.string(),
feature_six: z.string(),
});
// Combined schema that accepts either format
export const productMetadataSchema = z.union([legacyMetadataSchema, newMetadataSchema])
.transform((data) => {
// If it's the new format, return transformed data
if ('feature_one' in data) {
return {
priceCardVariant: data.price_card_variant,
featureOne: data.feature_one,
featureTwo: data.feature_two,
featureThree: data.feature_three,
featureFour: data.feature_four,
featureFive: data.feature_five,
featureSix: data.feature_six,
};
}
// If it's the legacy format, transform to new structure
return {
priceCardVariant: data.price_card_variant,
featureOne: `Generate ${data.generated_images === 'enterprise' ? 'unlimited' : data.generated_images} banner images`,
featureTwo: `${data.image_editor} image editing features`,
featureThree: `${data.support_level} support`,
featureFour: 'Community access',
featureFive: 'Standard integrations',
featureSix: 'Basic API access',
};
});
export type ProductMetadata = z.infer<typeof productMetadataSchema>;
export type PriceCardVariant = z.infer<typeof priceCardVariantSchema>;
price-card.tsx
// @ts-nocheck
'use client';
import { useMemo, useState } from 'react';
import Link from 'next/link';
import { IoCheckmark } from 'react-icons/io5';
import { SexyBoarder } from '@/components/sexy-boarder';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PriceCardVariant, productMetadataSchema } from '../models/product-metadata';
import { BillingInterval, Price, ProductWithPrices } from '../types';
export function PricingCard({
product,
price,
createCheckoutAction,
}: {
product: ProductWithPrices;
price?: Price;
createCheckoutAction?: ({ price }: { price: Price }) => void;
}) {
const [billingInterval, setBillingInterval] = useState<BillingInterval>(
price ? (price.interval as BillingInterval) : 'month'
);
const currentPrice = useMemo(() => {
if (price) return price;
if (product.prices.length === 0) return null;
if (product.prices.length === 1) return product.prices[0];
return product.prices.find((price) => price.interval === billingInterval);
}, [billingInterval, price, product.prices]);
const monthPrice = product.prices.find((price) => price.interval === 'month')?.unit_amount;
const yearPrice = product.prices.find((price) => price.interval === 'year')?.unit_amount;
const isBillingIntervalYearly = billingInterval === 'year';
const metadata = productMetadataSchema.parse(product.metadata);
const buttonVariantMap = {
basic: 'default',
pro: 'sexy',
enterprise: 'orange',
} as const;
function handleBillingIntervalChange(billingInterval: BillingInterval) {
setBillingInterval(billingInterval);
}
return (
<WithSexyBorder variant={metadata.priceCardVariant} className='w-full flex-1'>
<div className='flex w-full flex-col rounded-md border border-zinc-800 bg-black p-4 lg:p-8'>
<div className='p-4'>
<div className='mb-1 text-center font-alt text-xl font-bold'>{product.name}</div>
<div className='flex justify-center gap-0.5 text-zinc-400'>
<span className='font-semibold'>
{yearPrice && isBillingIntervalYearly
? '$' + yearPrice / 100
: monthPrice
? '$' + monthPrice / 100
: 'Custom'}
</span>
<span>{yearPrice && isBillingIntervalYearly ? '/year' : monthPrice ? '/month' : null}</span>
</div>
</div>
{!Boolean(price) && product.prices.length > 1 && <PricingSwitch onChange={handleBillingIntervalChange} />}
<div className='m-auto flex w-fit flex-1 flex-col gap-2 px-8 py-4'>
<CheckItem text={metadata.featureOne} />
<CheckItem text={metadata.featureTwo} />
<CheckItem text={metadata.featureThree} />
<CheckItem text={metadata.featureFour} />
<CheckItem text={metadata.featureFive} />
<CheckItem text={metadata.featureSix} />
</div>
{createCheckoutAction && (
<div className='py-4'>
{currentPrice && (
<Button
variant={buttonVariantMap[metadata.priceCardVariant]}
className='w-full'
onClick={() => createCheckoutAction({ price: currentPrice })}
>
Get Started
</Button>
)}
{!currentPrice && (
<Button variant={buttonVariantMap[metadata.priceCardVariant]} className='w-full' asChild>
<Link href='/contact'>Contact Us</Link>
</Button>
)}
</div>
)}
</div>
</WithSexyBorder>
);
}
function CheckItem({ text }: { text: string }) {
return (
<div className='flex items-center gap-2'>
<IoCheckmark className='my-auto flex-shrink-0 text-slate-500' />
<p className='text-sm font-medium text-white first-letter:capitalize'>{text}</p>
</div>
);
}
export function WithSexyBorder({
variant,
className,
children,
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { variant: PriceCardVariant }) {
if (variant === 'pro') {
return (
<SexyBoarder className={className} offset={100}>
{children}
</SexyBoarder>
);
} else {
return <div className={className}>{children}</div>;
}
}
function PricingSwitch({ onChange }: { onChange: (value: BillingInterval) => void }) {
return (
<Tabs
defaultValue='month'
className='flex items-center'
onValueChange={(newBillingInterval) => onChange(newBillingInterval as BillingInterval)}
>
<TabsList className='m-auto'>
<TabsTrigger value='month'>Monthly</TabsTrigger>
<TabsTrigger value='year'>Yearly</TabsTrigger>
</TabsList>
</Tabs>
);
}
stripe-fixtures.json
{
"_meta": {
"template_version": 0
},
"fixtures": [
{
"name": "basic",
"path": "/v1/products",
"method": "post",
"params": {
"name": "PLENTY Plan",
"description": "Access all of Virten.App.",
"metadata": {
"price_card_variant": "basic",
"feature_one": "Virten Prompt Library",
"feature_two": "Alphatutor",
"feature_three": "HTML Document Creator",
"feature_four": "Info Gatherer",
"feature_five": "Monthly Virten Customer Digest",
"feature_six": "Fund all Virten offerings"
}
}
},
{
"name": "price_basic_month",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${basic:id}",
"currency": "usd",
"billing_scheme": "per_unit",
"unit_amount": 999,
"recurring": {
"interval": "month",
"interval_count": 1
}
}
},
{
"name": "price_basic_year",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${basic:id}",
"currency": "usd",
"billing_scheme": "per_unit",
"unit_amount": 9999,
"recurring": {
"interval": "year",
"interval_count": 1
}
}
},
{
"name": "pro",
"path": "/v1/products",
"method": "post",
"params": {
"name": "EVERYTHING Plan",
"description": "Attain to VPL Superuser Access.",
"metadata": {
"price_card_variant": "pro",
"feature_one": "Everything in the PLENTY Plan",
"feature_two": "VPL Superuser Access",
"feature_three": "Early Access to New Prompts",
"feature_four": "VPL PWA Local App",
"feature_five": "Access to Halls.Virten Advanced Webapp Resources",
"feature_six": "Fund all Virten offerings"
}
}
},
{
"name": "price_pro_month",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${pro:id}",
"currency": "usd",
"billing_scheme": "per_unit",
"unit_amount": 1999,
"recurring": {
"interval": "month",
"interval_count": 1
}
}
},
{
"name": "price_pro_year",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${pro:id}",
"currency": "usd",
"billing_scheme": "per_unit",
"unit_amount": 19999,
"recurring": {
"interval": "year",
"interval_count": 1
}
}
}
]
}
I designed a prompt so the AI generated a document to outline the changes,
# Flexible Feature Fields Implementation
## Changes Made
1. **product-metadata.ts**
- Replaced all specific metadata fields with six generic feature fields
- Added transform to convert feature fields into an array
- Maintained price_card_variant for styling purposes
- All features are now simple strings, allowing for maximum flexibility
2. **price-card.tsx**
- Replaced specific feature rendering with dynamic feature array mapping
- Simplified CheckItem rendering to handle any feature text
- Maintained styling and layout consistency
3. **stripe-fixtures.json**
- Updated all product metadata to use new feature_1 through feature_6 fields
- Added example features to demonstrate usage
- Maintained price_card_variant for styling
## Benefits
- Complete flexibility in feature description
- No schema changes needed for feature updates
- Consistent display of all features
- Maintainable and scalable solution
## Usage Example
To modify features, simply update the feature fields in stripe-fixtures.json:
```json
"metadata": {
"price_card_variant": "basic",
"featureone": "Your custom feature 1",
"feature_two": "Your custom feature 2",
...
}
(the prompt to get nice docs from the AI is in my Virten Prompt Library product I will be selling via this helpful starter kit)
https://vpl.virten.app
I added '// @ts-nocheck' to the beginning of each file. You can rewrite the files to be typescript safe.
Hope this help you.
P.S. Kolby if you'd like to incorporate a link to this post in the starter kit docs or otherwise incorporate the 6 features option you're welcome to do so.
Metadata
Metadata
Assignees
Labels
No labels