diff --git a/README.md b/README.md index 4da0dc7..9db737d 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,14 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the ToggleShop leverages a number of feature flags for various technical and business-related use cases. -| Feature Flag | Type | Default Variant | Variants | -| ------------------- | ------- | --------------- | ----------- | -| offer-free-shipping | boolean | true | true, false | -| use-distributed-db | boolean | false | true, false | -| use-secure-protocol | boolean | false | true, false | - -> The flag configuration can be found [here](./flags.json). +| Feature Flag | Type | Default Variant | Variants | +| ------------------- | ------- | --------------- | -------- | +| offer-free-shipping | boolean | on | on, off | +| sticky-header | boolean | off | on, off | +| use-distributed-db | boolean | off | on, off | +| use-secure-protocol | boolean | off | on, off | + +> The flag configuration for local development can be found [here](./flags.json). ### Free Shipping diff --git a/flags.json b/flags.json index 06d2ba4..31def5d 100644 --- a/flags.json +++ b/flags.json @@ -4,32 +4,43 @@ "offer-free-shipping": { "state": "ENABLED", "variants": { - "true": true, - "false": false + "on": true, + "off": false }, - "defaultVariant": "true" + "defaultVariant": "on" + }, + "sticky-header": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on", + "targeting": { + "if": [{ "==": [{ "var": "size" }, "sm"]}, "on", "off" ] + } }, "use-distributed-db": { "state": "ENABLED", "variants": { - "true": true, - "false": false + "on": true, + "off": false }, "targeting": { "fractional": [ - ["true", 0], - ["false", 100] + ["on", 0], + ["off", 100] ] }, - "defaultVariant": "false" + "defaultVariant": "off" }, "use-secure-protocol": { "state": "ENABLED", "variants": { - "true": true, - "false": false + "on": true, + "off": false }, - "defaultVariant": "false" + "defaultVariant": "off" } } } diff --git a/kubernetes/toggle-shop.yaml b/kubernetes/toggle-shop.yaml index e1af4ce..9c1b7dd 100644 --- a/kubernetes/toggle-shop.yaml +++ b/kubernetes/toggle-shop.yaml @@ -14,6 +14,19 @@ spec: 'on': true 'off': false defaultVariant: 'on' + sticky-header: + state: ENABLED + variants: + 'on': true + 'off': false + defaultVariant: 'off' + # targeting: + # if: + # - "==": + # - var: size + # - sm + # - 'on' + # - 'off' --- # Flags for our backend application apiVersion: core.openfeature.dev/v1beta1 diff --git a/src/app/api/ofrep/v1/evaluate/flags/route.ts b/src/app/api/ofrep/v1/evaluate/flags/route.ts index 8d399f4..723fd6b 100644 --- a/src/app/api/ofrep/v1/evaluate/flags/route.ts +++ b/src/app/api/ofrep/v1/evaluate/flags/route.ts @@ -29,7 +29,7 @@ if (process.env.FLAGD_OFFLINE_FLAG_SOURCE_PATH) { } export async function POST(request: Request) { - const context = await request.json(); + const { context } = await request.json(); // We have to map these names to be compatible with OFREP const flags = flagdCore diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 51a44f1..84a04f2 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,19 +2,22 @@ import Link from "next/link"; import { ShoppingCart, Menu, X } from "lucide-react"; -import { useState } from "react"; +import { CSSProperties, useState } from "react"; import { useCart } from "@/hooks/use-cart"; +import { useFlag } from "@openfeature/react-sdk"; export default function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); const { cartItems } = useCart(); + const { value: stickyHeader } = useFlag('sticky-header', false); + const headerStyle: CSSProperties = stickyHeader ? { position: 'sticky', top: 0, zIndex: 1000 } : {}; const cartItemsCount = cartItems.reduce( (total, item) => total + item.quantity, 0 ); return ( -
+
diff --git a/src/hooks/use-products.tsx b/src/hooks/use-products.tsx index 9e49011..7a52390 100644 --- a/src/hooks/use-products.tsx +++ b/src/hooks/use-products.tsx @@ -6,10 +6,10 @@ import { getBaseUrl } from "@/libs/url"; import { tanstackMetaToHeader } from "@/libs/open-feature/evaluation-context"; export function useProducts() { - console.log("fetching products"); const { data } = useSuspenseQuery({ queryKey: ["products"], queryFn: async ({ meta }): Promise => { + console.log("fetching products"); const res = await fetch(getBaseUrl() + "/api/products", { cache: "no-store", headers: tanstackMetaToHeader(meta), @@ -24,10 +24,10 @@ export function useProducts() { } export function useProduct(id: string) { - console.log(`fetching product ${id}`); const { data } = useSuspenseQuery({ queryKey: ["products", id], queryFn: async ({ meta }): Promise => { + console.log(`fetching product ${id}`); const res = await fetch(getBaseUrl() + `/api/products/${id}`, { cache: "no-store", headers: tanstackMetaToHeader(meta), diff --git a/src/hooks/use-size.tsx b/src/hooks/use-size.tsx new file mode 100644 index 0000000..d867cc9 --- /dev/null +++ b/src/hooks/use-size.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type Size = "sm" | "md" | "lg" | "xl"; + +const computeSize = (): Size => { + return globalThis.innerWidth < 768 ? 'sm' : globalThis.innerWidth < 992 ? 'md' : globalThis.innerWidth < 1200 ? 'lg' : 'xl'; +} + +export function useSize() { + + const [size, setSize] = useState(computeSize()); + const resizeListener = () => { + const newSize = computeSize(); + setSize(newSize); + }; + + useEffect(() => { + globalThis.addEventListener('resize', resizeListener); + return () => { + globalThis.removeEventListener('resize', resizeListener) + }; + }, []); + + return size; + +} diff --git a/src/providers/open-feature.tsx b/src/providers/open-feature.tsx index 882867c..f77217c 100644 --- a/src/providers/open-feature.tsx +++ b/src/providers/open-feature.tsx @@ -12,6 +12,7 @@ import { useEffect, useRef } from "react"; import { ClientEventHook } from "@/libs/open-feature/client-event-hook"; import { getBaseUrl } from "@/libs/url"; import { ATTR_FEATURE_FLAG_CONTEXT_ID } from "@/libs/open-feature/proposed-attributes"; +import { useSize } from "@/hooks/use-size"; class OFREPWebEventProvider extends OFREPWebProvider implements Provider { metadata = { name: "OREFP" }; @@ -47,6 +48,13 @@ export function OpenFeatureProvider({ children: React.ReactNode; }) { const hasInitialized = useRef(false); + const size = useSize(); + + useEffect(() => { + if (hasInitialized.current) { + OpenFeature.setContext({ ...OpenFeature.getContext(), size }); + } + }, [size]) useEffect(() => { if (!hasInitialized.current) { @@ -59,7 +67,7 @@ export function OpenFeatureProvider({ // A real app may want to only update on page load. pollInterval: 5000, }), - context + {...context, size} ); hasInitialized.current = true; }