Skip to content

Set Of Files To Replace Price Card Metadata With Six Features You Can Enter On the stripe-fixtures File #11

@traflagar

Description

@traflagar

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

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions