Skip to content

Commit

Permalink
convert summer-sale to shirt-shop
Browse files Browse the repository at this point in the history
  • Loading branch information
dferber90 committed Feb 28, 2025
1 parent bdbaa46 commit 528b7de
Show file tree
Hide file tree
Showing 67 changed files with 1,837 additions and 1,285 deletions.
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"summer-sale",
"shirt-shop",
"svelte-example",
"next-13",
"next-14",
Expand Down
File renamed without changes.
File renamed without changes.
56 changes: 56 additions & 0 deletions examples/shirt-shop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Shirt Shop Flags SDK Example

This example uses the [Flags SDK](https://flags-sdk.dev) along with the [Flags Explorer](https://vercel.com/docs/workflow-collaboration/feature-flags/using-vercel-toolbar) and `@vercel/analytics`.

## Demo

[https://shirt-shop.labs.vercel.dev/](https://shirt-shop.labs.vercel.dev/)

## How it works

This demo uses two feature flags defined in code control the visibility of two banners on the page.
Both flags are configured to show/hide each banner 50% of the time.

Once you visit the page, you can see a variation of both/one/none of the banners.
Since this example is using a stable id to identify users, you will see the same variation all the time.

To test different variations, you can use the Dev Tools at the bottom to reset the stable id and reload the page.
This allows you to test different variations of the banners.

This templates also tracks analytics events in Vercel Analytics.

If you deployed your own instance of this example you can also use the [Flags Explorer](https://vercel.com/docs/workflow-collaboration/feature-flags/using-vercel-toolbar) to test different variations by creating overrides.

## Deploy this template

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fflags%2Ftree%2Fmain%2Fexamples%2Fshirt-shop&env=FLAGS_SECRET&envDescription=The+FLAGS_SECRET+will+be+used+by+the+Flags+Explorer+to+securely+overwrite+feature+flags.+Must+be+32+random+bytes%2C+base64-encoded.+Use+the+generated+value+or+set+your+own.&envLink=https%3A%2F%2Fvercel.com%2Fdocs%2Fworkflow-collaboration%2Ffeature-flags%2Fsupporting-feature-flags%23flags_secret-environment-variable&project-name=shirt-shop-flags-sdk-example&repository-name=shirt-shop-flags-sdk-example)

### Step 1: Link the project

In order to use the Flags Explorer, you need to link the project on your local machine.

```bash
vercel link
```

Select the project from the list you just deployed.

### Step 2: Pull all environment variables

This allows the Flags SDK and the Flags Explorer to work correctly, by getting additional metadata.

```bash
vercel env pull
```

### Step 2: Install dependencies

```bash
npm install
```

### Step 3: Run the project

```bash
npm run dev
```
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export async function GET(request: NextRequest) {
const access = await verifyAccess(request.headers.get('Authorization'));
if (!access) return NextResponse.json(null, { status: 401 });

const providerData = getProviderData(flags);
// Forward info from Flags in Code
const providerData = await getProviderData(flags);
return NextResponse.json<ApiData>(providerData);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { precomputeFlags } from '@/flags';
import { type FlagValuesType, encrypt } from 'flags';
import { productFlags } from '@/flags';
import { deserialize, generatePermutations } from 'flags/next';
import { FlagValues } from 'flags/react';
import { Suspense } from 'react';

async function ConfidentialFlagValues({ values }: { values: FlagValuesType }) {
const encryptedFlagValues = await encrypt(values);
return <FlagValues values={encryptedFlagValues} />;
}

export async function generateStaticParams() {
// Returning an empty array here is important as it enables ISR, so
Expand All @@ -17,7 +10,7 @@ export async function generateStaticParams() {

// Instead of returning an empty array you could also call generatePermutations
// to generate the permutations upfront.
const codes = await generatePermutations(precomputeFlags);
const codes = await generatePermutations(productFlags);
return codes.map((code) => ({ code }));
}

Expand All @@ -28,17 +21,12 @@ export default async function Layout(props: {
}>;
}) {
const params = await props.params;

const { children } = props;

const values = await deserialize(precomputeFlags, params.code);
const values = await deserialize(productFlags, params.code);

return (
<>
{children}
<Suspense fallback={null}>
<ConfidentialFlagValues values={values} />
</Suspense>
{props.children}
<FlagValues values={values} />
</>
);
}
54 changes: 54 additions & 0 deletions examples/shirt-shop/app/[code]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AddToCartButton } from '@/components/add-to-cart-button';
import { FreeDeliveryBanner } from '@/components/banners/free-delivery-banner';
import { SummerBanner } from '@/components/banners/summer-banner';
import { ColorPicker } from '@/components/color-picker';
import { DevTools } from '@/components/dev-tools';
import { Footer } from '@/components/footer';
import { ImageGallery } from '@/components/image-gallery';
import { Navigation } from '@/components/navigation';
import { ProductDetails } from '@/components/product-details';
import { ProductHeader } from '@/components/product-header';
import { SizePicker } from '@/components/size-picker';
import {
productFlags,
showFreeDeliveryBannerFlag,
showSummerBannerFlag,
} from '@/flags';
import { getPrecomputed } from 'flags/next';

export default async function Page(props: {
params: Promise<{ code: string }>;
}) {
const params = await props.params;

const [showSummerBanner, showFreeDeliveryBanner] = await getPrecomputed(
[showSummerBannerFlag, showFreeDeliveryBannerFlag],
productFlags,
params.code,
);

return (
<div className="bg-white">
<FreeDeliveryBanner show={showFreeDeliveryBanner} />
<Navigation />
<SummerBanner show={showSummerBanner} />

<main className="mx-auto max-w-2xl px-4 pb-16 sm:px-6 sm:pb-24 lg:max-w-7xl lg:px-8">
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
<ProductHeader />
<ImageGallery />

<div className="mt-8 lg:col-span-5">
<ColorPicker />
<SizePicker />
<AddToCartButton />
<ProductDetails />
</div>
</div>
</main>

<Footer />
<DevTools />
</div>
);
}
Binary file added examples/shirt-shop/app/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions examples/shirt-shop/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'tailwindcss';
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import type { Metadata } from 'next';
import { VercelToolbar } from '@vercel/toolbar/next';
import { Analytics } from '@vercel/analytics/next';
import type { Metadata } from 'next';

import './globals.css';

export const metadata: Metadata = {
title: 'Shirt Shop',
description: 'Generated by create next app',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const shouldInjectToolbar = process.env.NODE_ENV === 'development';
return (
<html lang="en">
<body className="">
{children}

{shouldInjectToolbar && <VercelToolbar />}
<Analytics />
<VercelToolbar />
</body>
</html>
);
Expand Down
20 changes: 20 additions & 0 deletions examples/shirt-shop/components/add-to-cart-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import { track } from '@vercel/analytics';
import { useEffect } from 'react';

export function AddToCartButton() {
useEffect(() => {
track('add_to_cart:viewed');
}, []);

return (
<button
type="button"
className="mt-8 flex w-full items-center justify-center rounded-full border border-transparent bg-blue-600 px-8 py-3 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={() => track('add_to_cart:clicked')}
>
Add to cart
</button>
);
}
18 changes: 18 additions & 0 deletions examples/shirt-shop/components/banners/free-delivery-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { track } from '@vercel/analytics';
import { useEffect } from 'react';

export function FreeDeliveryBanner(props: { show: boolean }) {
useEffect(() => {
if (props.show) track('free_delivery_banner:viewed');
}, [props.show]);

if (!props.show) return null;

return (
<div className="px-4 py-2 bg-gray-950 text-white text-center text text-sm font-medium">
Get free delivery on orders over $100
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
'use client';

import pool from '@/public/images/pool.jpg';
import Image from 'next/image';
import summer from '../public/etienne-girardet-Xh6BpT-1tXo-unsplash.jpg';
import { track } from '@vercel/analytics/react';
import { track } from '@vercel/analytics';
import { useEffect } from 'react';

export function SummerBanner(props: { show: boolean }) {
useEffect(() => {
if (props.show) track('summer_banner:viewed');
}, [props.show]);

if (!props.show) return null;

export function Promo() {
return (
<div className="bg-white">
<div className="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl px-4 pb-8 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-lg">
<div className="absolute inset-0">
<Image
src={summer}
alt=""
src={pool}
alt="Summer Sale"
className="h-full w-full object-cover object-center"
/>
</div>
Expand All @@ -22,19 +29,15 @@ export function Promo() {
<span className="block sm:inline">Summer Sale</span>
</h2>
<p className="mt-3 text-xl text-white">
Enjoy 20% off all summer styles,
Enjoy 20% off all summer basics,
<br />
from bright dresses to pastel-hued tops.
including swimwear and accessories.
</p>
<button
type="button"
className="mt-8 block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
onClick={() => {
track(
'Clicked Shop Now',
{},
{ flagKeys: ['summer-sale', 'free-delivery'] },
);
track('summer_banner:clicked');
}}
>
Shop now
Expand Down
48 changes: 48 additions & 0 deletions examples/shirt-shop/components/color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client';

import { Radio, RadioGroup } from '@headlessui/react';
import clsx from 'clsx';
import { useState } from 'react';

const colors = [
{ name: 'Black', bgColor: 'bg-gray-900', selectedColor: 'ring-gray-900' },
{ name: 'White', bgColor: 'bg-white', selectedColor: 'ring-gray-300' },
{ name: 'Blue', bgColor: 'bg-blue-500', selectedColor: 'ring-blue-500' },
];

export function ColorPicker() {
const [selectedColor, setSelectedColor] = useState(colors[0]);

return (
<div>
<h2 className="text-sm font-medium text-gray-900">Color</h2>
<fieldset aria-label="Choose a color" className="mt-2">
<RadioGroup
value={selectedColor}
onChange={setSelectedColor}
className="flex items-center gap-x-3"
>
{colors.map((color) => (
<Radio
key={color.name}
value={color}
aria-label={color.name}
className={clsx(
color.selectedColor,
'relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-hidden data-checked:ring-2 data-focus:data-checked:ring-3 data-focus:data-checked:ring-offset-1',
)}
>
<span
aria-hidden="true"
className={clsx(
color.bgColor,
'size-8 rounded-full border border-black/10',
)}
/>
</Radio>
))}
</RadioGroup>
</fieldset>
</div>
);
}
28 changes: 28 additions & 0 deletions examples/shirt-shop/components/dev-tools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useRouter } from 'next/navigation';

export function DevTools() {
const router = useRouter();

const deleteCookie = () => {
document.cookie =
'stable-id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
router.refresh();
};

return (
<div className="fixed bottom-2 right-2 p-3 bg-[#333333] rounded shadow-lg z-50 flex flex-col gap-2">
<span className="text-white font-mono text-xs">Dev Tools</span>
<button
type="button"
className="bg-white text-black font-mono text-xs rounded px-2 py-1 flex items-center gap-2 cursor-pointer hover:bg-gray-200 transition-colors active:bg-gray-300"
onClick={deleteCookie}
>
<ArrowPathIcon className="w-4 h-4" />
Reset Stable ID
</button>
</div>
);
}
Loading

0 comments on commit 528b7de

Please sign in to comment.