Skip to content

Commit 5e9e847

Browse files
roncohenAAorris
andauthored
[flags-sdk] Bucket example (#1128)
### Description Flags SDK example using Bucket from https://bucket.co/ --------- Co-authored-by: Aaron Morris <[email protected]>
1 parent e5d13f1 commit 5e9e847

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+5950
-0
lines changed

flags-sdk/bucket/.eslintrc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"root": true,
3+
"extends": "next/core-web-vitals",
4+
"rules": {
5+
"@typescript-eslint/require-await": "off",
6+
"@typescript-eslint/no-misused-promises": "off",
7+
"import/order": "off",
8+
"camelcase": "off",
9+
"no-console": "off"
10+
}
11+
}

flags-sdk/bucket/.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

flags-sdk/bucket/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Bucket Flags SDK Example
2+
3+
This example uses [Bucket](https://bucket.co) for feature flags with the [Flags SDK](https://flags-sdk.dev) along with the `@flags-sdk/bucket` [Bucket adapter](https://flags-sdk.dev/docs/api-reference/adapters/bucket) and the [Flags Explorer](https://vercel.com/docs/workflow-collaboration/feature-flags/using-vercel-toolbar).
4+
5+
## Demo
6+
7+
[https://flags-sdk-bucket.vercel.app/](https://flags-sdk-bucket.vercel.app/)
8+
9+
## How it works
10+
11+
This demo uses two features on Bucket to control the visibility of two banners on the page.
12+
Both gates are configured to show/hide each banner 50% of the time.
13+
14+
If you deployed your own and configured the features on Bucket, you can also use the [Flags Explorer](https://vercel.com/docs/workflow-collaboration/feature-flags/using-vercel-toolbar) to enabled/disabled the features.
15+
16+
## Deploy this template
17+
18+
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fflags-sdk/bucket&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=bucket-flags-sdk-example&repository-name=bucket-flags-sdk-example)
19+
20+
### Step 1: Link the project
21+
22+
In order to use the Flags Explorer, you need to link the project on your local machine.
23+
24+
```bash
25+
vercel link
26+
```
27+
28+
Select the project from the list you just deployed.
29+
30+
### Step 2: Pull all environment variables
31+
32+
This allows the Flags SDK and the Flags Explorer to work correctly, by getting additional metadata.
33+
34+
```bash
35+
vercel env pull
36+
```
37+
38+
### Step 3: Create Features
39+
40+
Head over to the [Bucket application](app.bucket.co) and create the features required by this template.
41+
42+
Features:
43+
44+
- `Summer Sale` with the feature key `summer_sale`
45+
- `Free Shipping` with the feature key `free_delivery`
46+
47+
You can also find the feature keys in the `flags.ts` file.
48+
49+
### Step 4: Configure the Features
50+
51+
Select the `Summer Sale` and `Free Shipping` features and configure them on the Bucket Console.
52+
53+
Create a new rule by clicking on "+ Add Rule" and set the percentage to 50%.
54+
55+
After that, click on "Save".
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getProviderData, createFlagsDiscoveryEndpoint } from 'flags/next'
2+
import { getProviderData as getBucketProviderData } from '@flags-sdk/bucket'
3+
import { mergeProviderData } from 'flags'
4+
import * as flags from '../../../../flags'
5+
6+
export const dynamic = 'force-dynamic' // defaults to auto
7+
8+
export const GET = createFlagsDiscoveryEndpoint(async (request) => {
9+
return mergeProviderData([
10+
// Data declared from Flags in Code
11+
getProviderData(flags),
12+
// metadata from Bucket API using the default bucket adapter
13+
getBucketProviderData(),
14+
])
15+
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { addToCart } from '@/lib/actions'
6+
import { useProductDetailPageContext } from '@/components/utils/product-detail-page-context'
7+
import { AddToCartButton } from '@/components/product-detail-page/add-to-cart-button'
8+
9+
export function AddToCart() {
10+
const router = useRouter()
11+
const { color, size } = useProductDetailPageContext()
12+
const [isLoading, setIsLoading] = useState(false)
13+
14+
return (
15+
<AddToCartButton
16+
isLoading={isLoading}
17+
onClick={async () => {
18+
setIsLoading(true)
19+
await addToCart({ id: 'shirt', color, size, quantity: 1 })
20+
router.push('/cart')
21+
}}
22+
/>
23+
)
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'
2+
import { ProceedToCheckout } from './proceed-to-checkout'
3+
4+
export async function OrderSummary({
5+
showSummerBanner,
6+
freeDelivery,
7+
}: {
8+
showSummerBanner: boolean
9+
freeDelivery: boolean
10+
}) {
11+
return (
12+
<OrderSummarySection
13+
showSummerBanner={showSummerBanner}
14+
freeDelivery={freeDelivery}
15+
proceedToCheckout={
16+
<ProceedToCheckout color={'blue'} experiment="proceed_to_checkout" />
17+
}
18+
/>
19+
)
20+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { OrderSummary } from '@/app/[code]/cart/order-summary'
2+
import { Main } from '@/components/main'
3+
import { ShoppingCart } from '@/components/shopping-cart/shopping-cart'
4+
import {
5+
productFlags,
6+
showFreeDeliveryBannerFlag,
7+
showSummerBannerFlag,
8+
} from '@/flags'
9+
10+
export default async function CartPage({
11+
params,
12+
}: {
13+
params: Promise<{ code: string }>
14+
}) {
15+
const { code } = await params
16+
const showSummerBanner = await showSummerBannerFlag(code, productFlags)
17+
const freeDeliveryBanner = await showFreeDeliveryBannerFlag(
18+
code,
19+
productFlags
20+
)
21+
22+
return (
23+
<Main>
24+
<div className="lg:grid lg:grid-cols-12 lg:items-start lg:gap-x-12 xl:gap-x-16">
25+
<ShoppingCart />
26+
<OrderSummary
27+
showSummerBanner={showSummerBanner}
28+
freeDelivery={freeDeliveryBanner}
29+
/>
30+
</div>
31+
</Main>
32+
)
33+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
3+
import { ProceedToCheckoutButton } from '@/components/shopping-cart/proceed-to-checkout-button'
4+
5+
import { toast } from 'sonner'
6+
7+
export function ProceedToCheckout({
8+
color,
9+
experiment,
10+
}: {
11+
color: string
12+
experiment: string
13+
}) {
14+
return (
15+
<>
16+
<ProceedToCheckoutButton
17+
color={color}
18+
onClick={() => {
19+
// Auto capture will track the event
20+
toast('End reached', {
21+
className: 'my-classname',
22+
description:
23+
'The checkout flow is not implemented in this template.',
24+
duration: 5000,
25+
})
26+
}}
27+
/>
28+
</>
29+
)
30+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { deserialize, generatePermutations } from 'flags/next'
2+
import { FlagValues } from 'flags/react'
3+
import { productFlags, showFreeDeliveryBannerFlag } from '@/flags'
4+
import { FreeDelivery } from '@/app/free-delivery'
5+
import { DevTools } from '@/components/dev-tools'
6+
import { Footer } from '@/components/footer'
7+
import { Navigation } from '@/components/navigation'
8+
9+
export async function generateStaticParams() {
10+
// Returning an empty array here is important as it enables ISR, so
11+
// the various combinations stay cached after they first time they were rendered.
12+
//
13+
// return [];
14+
15+
// Instead of returning an empty array you could also call generatePermutations
16+
// to generate the permutations upfront.
17+
const codes = await generatePermutations(productFlags)
18+
return codes.map((code) => ({ code }))
19+
}
20+
21+
export default async function Layout(props: {
22+
children: React.ReactNode
23+
params: Promise<{
24+
code: string
25+
}>
26+
}) {
27+
const params = await props.params
28+
const values = await deserialize(productFlags, params.code)
29+
30+
const showFreeDeliveryBanner = await showFreeDeliveryBannerFlag(
31+
params.code,
32+
productFlags
33+
)
34+
35+
return (
36+
<div className="bg-white">
37+
<FreeDelivery show={showFreeDeliveryBanner} />
38+
<Navigation />
39+
{props.children}
40+
<FlagValues values={values} />
41+
<Footer />
42+
<DevTools />
43+
</div>
44+
)
45+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { SummerSale } from '@/app/summer-sale'
2+
import { ImageGallery } from '@/components/image-gallery'
3+
import { ProductDetails } from '@/components/product-detail-page/product-details'
4+
import { ProductHeader } from '@/components/product-detail-page/product-header'
5+
import { AddToCart } from '@/app/[code]/add-to-cart'
6+
import { ColorPicker } from '@/components/product-detail-page/color-picker'
7+
import { SizePicker } from '@/components/product-detail-page/size-picker'
8+
import { ProductDetailPageProvider } from '@/components/utils/product-detail-page-context'
9+
10+
import { productFlags, showSummerBannerFlag } from '@/flags'
11+
import { Main } from '@/components/main'
12+
13+
export default async function Page(props: {
14+
params: Promise<{ code: string }>
15+
}) {
16+
const params = await props.params
17+
const showSummerBanner = await showSummerBannerFlag(params.code, productFlags)
18+
19+
return (
20+
<ProductDetailPageProvider>
21+
<SummerSale show={showSummerBanner} gate={showSummerBannerFlag.key} />
22+
<Main>
23+
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
24+
<ProductHeader />
25+
<ImageGallery />
26+
27+
<div className="mt-8 lg:col-span-5">
28+
<ColorPicker />
29+
<SizePicker />
30+
<AddToCart />
31+
<ProductDetails />
32+
</div>
33+
</div>
34+
</Main>
35+
</ProductDetailPageProvider>
36+
)
37+
}

0 commit comments

Comments
 (0)