├── .gitignore ├── vite.config.js ├── src ├── main.jsx ├── index.css ├── App.jsx ├── App.css ├── favicon.svg └── Pokemon.jsx ├── index.html ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import reactRefresh from '@vitejs/plugin-react-refresh' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [reactRefresh()] 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import "./index.css"; 5 | import App from "./App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokemon-feature-flags", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview" 8 | }, 9 | "dependencies": { 10 | "launchdarkly-react-client-sdk": "^2.22.2", 11 | "react": "^17.0.0", 12 | "react-dom": "^17.0.0" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-react-refresh": "^1.3.1", 16 | "vite": "^2.3.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Pokemon from "./Pokemon"; 3 | import { withLDProvider } from "launchdarkly-react-client-sdk"; 4 | 5 | function App() { 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | } 12 | 13 | export default withLDProvider({ 14 | clientSideID: import.meta.env 15 | .VITE_LD_CLIENT_KEY, 16 | user: { 17 | key: "user_key", 18 | name: "User name", 19 | email: "User@email.com", 20 | }, 21 | })(App); 22 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Arial", sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | font-size: 20px; 7 | } 8 | 9 | .pokemon { 10 | position: relative; 11 | margin: 20px auto; 12 | width: 400px; 13 | height: 400px; 14 | border: 10px solid #282828; 15 | border-radius: 50%; 16 | overflow: hidden; 17 | text-align: center; 18 | } 19 | 20 | .pokemon div { 21 | display: flex; 22 | align-items: center; 23 | margin-bottom: 40px; 24 | background: #d5615e; 25 | height: 50%; 26 | } 27 | .pokemon input { 28 | margin: auto; 29 | padding: 5px; 30 | display: block; 31 | border: 3px solid #282828; 32 | border-radius: 10px; 33 | font-size: 20px; 34 | &:focus { 35 | outline: 3px dashed #282828; 36 | } 37 | } 38 | 39 | .pokemon span { 40 | text-transform: capitalize; 41 | } 42 | 43 | .pokemon img { 44 | margin: auto; 45 | display: block; 46 | } 47 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pokémon Feature Flags demo 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/9242ef3b-3f43-4153-8d3f-65054c333a43/deploy-status)](https://app.netlify.com/sites/pokemon-ld/deploys) 4 | 5 | Here's a demo for integrating feature flags into a React project! Built with React, Vite, [the PokeAPI](https://pokeapi.co/), and [LaunchDarkly](https://launchdarkly.com/)! 6 | 7 | When the feature flag is off, only Normal type Pokémon can be searched for in the app. When the flag is turned on, then any Pokémon can be queried! 8 | 9 | ## Deploy your own 10 | 11 | ### LaunchDarkly setup 12 | 13 | Once you've signed up for [LaunchDarkly](https://launchdarkly.com/), create a feature flag. I call mine `testaroni`, and you should too if you want this to work out of the box. Otherwise, you can change it in the `isAllowed` function in `Pokemon.jsx`. Under **Flag variations**, make it a **String** type and add an "all" type, and any other Pokémon types you'd like to include: 14 | 15 | ![image](https://user-images.githubusercontent.com/1454517/118706538-143c0580-b7df-11eb-88ef-08db3391e17d.png) 16 | 17 | After that, you're all set! You can toggle it on and off in the LaunchDarkly UI, set the default rules, segment which users get which types, and more. 18 | 19 | ### Netlify setup 20 | 21 | Click this button to deploy your own version of this project to Netlify! 22 | 23 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/cassidoo/pokemon-feature-flags&utm_source=github&utm_medium=ldstream-cs&utm_campaign=devex-cs) 24 | 25 | You'll need to add your environment variables after cloning. Structure your `.env.local` file like so (you can find your client ID under [Account Settings => Projects](https://app.launchdarkly.com/settings/projects)): 26 | 27 | ``` 28 | VITE_LD_CLIENT_KEY=your_key 29 | ``` 30 | 31 | ...and then plop it into Netlify, and you're done! 32 | -------------------------------------------------------------------------------- /src/Pokemon.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | } from "react"; 5 | import { withLDConsumer } from "launchdarkly-react-client-sdk"; 6 | 7 | import "./App.css"; 8 | 9 | const Pokemon = ({ flags }) => { 10 | const [pokémon, setPokémon] = 11 | useState("pidgey"); 12 | const [img, setImg] = useState("pidgey"); 13 | 14 | useEffect(() => { 15 | document.title = "Hello, " + pokémon; 16 | }, [pokémon]); 17 | 18 | useEffect(() => { 19 | let isCurrent = true; 20 | 21 | if (pokémon.length >= 4) { 22 | fetch( 23 | `https://pokeapi.co/api/v2/pokemon/${pokémon}/` 24 | ) 25 | .then((res) => res.json()) 26 | .then((res) => { 27 | if ( 28 | isCurrent && 29 | isAllowed(res.types, flags) 30 | ) { 31 | setPokémon( 32 | res.name.replace(/^\w/, (c) => 33 | c.toUpperCase() 34 | ) 35 | ); // capitalizing name 36 | setImg(res.sprites.front_default); 37 | } 38 | }) 39 | .catch((error) => { 40 | console.log(error); 41 | }); 42 | } 43 | 44 | return () => { 45 | isCurrent = false; 46 | }; 47 | }, [pokémon]); 48 | 49 | return ( 50 | <> 51 |
52 |
53 | 55 | setPokémon(e.target.value) 56 | } 57 | defaultValue={pokémon} 58 | type="text" 59 | /> 60 |
61 | Hello, {pokémon}! 62 | 63 |
64 | 65 | ); 66 | }; 67 | 68 | function isAllowed(types, flags) { 69 | return ( 70 | flags.testaroni === "all" || 71 | types 72 | .map((t) => { 73 | return t.type.name; 74 | }) 75 | .some((e) => { 76 | return e === flags.testaroni; 77 | }) 78 | ); 79 | } 80 | 81 | export default withLDConsumer()(Pokemon); 82 | --------------------------------------------------------------------------------