diff --git a/client-app/README.md b/client-app/README.md
new file mode 100644
index 0000000..76b0fff
--- /dev/null
+++ b/client-app/README.md
@@ -0,0 +1,73 @@
+# Quote of the Day
+
+A react application showing how to load a **variant feature flag** from **Azure App Configuration** via **Azure Front Door** and use it to experiment with a personalized greeting in a classic Quote of the Day UI.
+
+## Prerequisites
+* Node.js 18+
+* Azure Front Door endpoint configured with Azure App Configuration as its origin.
+* Application Insights resource
+
+## Get Started
+
+- Replace the placeholders in `src/config.js`:
+ ```js
+ // Application Insights connection string (Instrumentation for telemetry)
+ appInsightsConnectionString: "YOUR-APP-INSIGHTS-CONNECTION-STRING",
+ // Azure Front Door endpoint hosting App Configuration data (feature flags, etc.)
+ azureFrontDoorEndpoint: "YOUR-AZURE-FRONT-DOOR-ENDPOINT"
+ ```
+
+- Create a variant feature flag named `Greeting` in your App Configuration store with the below configuration:
+
+ ```json
+ {
+ "id": "Greeting",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "Off",
+ "configuration_value": false
+ },
+ {
+ "name": "On",
+ "configuration_value": true
+ }
+ ],
+ "allocation": {
+ "percentile": [
+ {
+ "variant": "Off",
+ "from": 0,
+ "to": 50
+ },
+ {
+ "variant": "On",
+ "from": 50,
+ "to": 100
+ }
+ ],
+ "user": [
+ {
+ "variant": "On",
+ "users": [
+ "admin"
+ ]
+ }
+ ],
+ "default_when_enabled": "Off",
+ "default_when_disabled": "Off"
+ },
+ "telemetry": {
+ "enabled": true
+ }
+ }
+ ```
+- Run the following command:
+
+ ```powershell
+ cd client-app
+ npm install
+ npm run build
+ npm run preview
+ ```
+
diff --git a/client-app/index.html b/client-app/index.html
new file mode 100644
index 0000000..149e009
--- /dev/null
+++ b/client-app/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Quote of the Day
+
+
+
+
+
+
diff --git a/client-app/package.json b/client-app/package.json
new file mode 100644
index 0000000..4a345db
--- /dev/null
+++ b/client-app/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "quote-of-the-day",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@azure/app-configuration-provider": "2.3.0-preview",
+ "@microsoft/feature-management": "latest",
+ "@microsoft/feature-management-applicationinsights-browser": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-icons": "5.3.0",
+ "react-router-dom": "^6.27.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.1",
+ "vite": "^7.2.2"
+ }
+}
diff --git a/client-app/public/vite.svg b/client-app/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/client-app/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client-app/src/App.css b/client-app/src/App.css
new file mode 100644
index 0000000..0df6d08
--- /dev/null
+++ b/client-app/src/App.css
@@ -0,0 +1,198 @@
+body {
+ margin: 0;
+ font-family: 'Georgia', serif;
+}
+
+.quote-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background-color: #f4f4f4;
+}
+
+.navbar {
+ background-color: white;
+ border-bottom: 1px solid #eaeaea;
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 20px;
+ align-items: center;
+ font-family: 'Arial', sans-serif;
+ font-size: 16px;
+}
+
+.navbar-left {
+ display: flex;
+ align-items: center;
+ margin-left: 40px;
+}
+
+.logo {
+ font-size: 1.25em;
+ text-decoration: none;
+ color: black;
+ margin-right: 20px;
+}
+
+.navbar-left nav a {
+ margin-right: 20px;
+ text-decoration: none;
+ color: black;
+ font-weight: 500;
+ font-family: 'Arial', sans-serif;
+}
+
+.navbar-right a {
+ margin-left: 20px;
+ text-decoration: none;
+ color: black;
+ font-weight: 500;
+ font-family: 'Arial', sans-serif;
+}
+
+.quote-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-grow: 1;
+}
+
+.quote-card {
+ background-color: white;
+ padding: 30px;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ max-width: 700px;
+ position: relative;
+ text-align: left;
+}
+
+.quote-card h2 {
+ font-weight: normal;
+}
+
+.quote-card blockquote {
+ font-size: 2em;
+ font-family: 'Georgia', serif;
+ font-style: italic;
+ color: #4EC2F7;
+ margin: 0 0 20px 0;
+ line-height: 1.4;
+ text-align: left;
+}
+
+.quote-card footer {
+ font-size: 0.55em;
+ color: black;
+ font-family: 'Arial', sans-serif;
+ font-style: normal;
+ text-align: left;
+ font-weight: bold;
+}
+
+.vote-container {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ display: flex;
+ gap: 0em;
+}
+
+.heart-button {
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 5px;
+ font-size: 24px;
+}
+
+.heart-button:hover {
+ background-color: #F0F0F0;
+}
+
+.heart-button:focus {
+ outline: none;
+ box-shadow: none;
+}
+
+footer {
+ background-color: white;
+ padding-top: 10px;
+ text-align: center;
+ border-top: 1px solid #eaeaea;
+}
+
+footer a {
+ color: #4EC2F7;
+ text-decoration: none;
+}
+
+.register-login-card {
+ width: 300px;
+ margin: 50px auto;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
+ background-color: #ffffff;
+ text-align: center;
+}
+
+h2 {
+ margin-bottom: 20px;
+ color: #333;
+}
+
+.input-container {
+ margin-bottom: 15px;
+ text-align: left;
+ width: 100%; /* Ensure the container takes the full width */
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+ font-size: 14px;
+ color: #555;
+}
+
+input {
+ width: calc(100%); /* Add padding for both left and right */
+ padding: 10px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-size: 14px;
+ box-sizing: border-box; /* Ensure padding doesn't affect the width */
+}
+
+input:focus {
+ outline: none;
+ border-color: #007bff;
+}
+
+.register-login-button {
+ width: 100%;
+ padding: 10px;
+ background-color: #007bff;
+ border: none;
+ border-radius: 4px;
+ color: white;
+ font-size: 16px;
+ cursor: pointer;
+ margin-top: 10px;
+}
+
+.register-login-button:hover {
+ background-color: #0056b3;
+}
+
+.error-message {
+ color: red;
+}
+
+.logout-btn {
+ margin-left: 20px;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+}
\ No newline at end of file
diff --git a/client-app/src/App.jsx b/client-app/src/App.jsx
new file mode 100644
index 0000000..4e5014d
--- /dev/null
+++ b/client-app/src/App.jsx
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
+import { ContextProvider } from "./pages/AppContext";
+import Layout from "./Layout";
+import Home from "./pages/Home";
+import Register from "./pages/Register";
+import Login from "./pages/Login";
+
+
+function App() {
+ return (
+
+
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client-app/src/Layout.jsx b/client-app/src/Layout.jsx
new file mode 100644
index 0000000..b6a3c2b
--- /dev/null
+++ b/client-app/src/Layout.jsx
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { useContext } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { AppContext } from "./pages/AppContext";
+
+const Layout = ({ children }) => {
+ const { currentUser, logoutUser } = useContext(AppContext);
+ const navigate = useNavigate();
+
+ const handleLogout = () => {
+ logoutUser();
+ navigate("/");
+ };
+
+ return (
+
+
+
+ QuoteOfTheDay
+
+
+
+ {currentUser ?
+ (
+ <>
+ Hello, {currentUser}!
+
+ >
+ ) :
+ (
+ <>
+ Register
+ Login
+ >
+ )
+ }
+
+
+
+
+ {children}
+
+
+
+
+ );
+};
+
+export default Layout;
\ No newline at end of file
diff --git a/client-app/src/config.js b/client-app/src/config.js
new file mode 100644
index 0000000..1dac8e1
--- /dev/null
+++ b/client-app/src/config.js
@@ -0,0 +1,6 @@
+export const config = {
+ // Application Insights connection string (Instrumentation for telemetry)
+ appInsightsConnectionString: "YOUR-APP-INSIGHTS-CONNECTION-STRING",
+ // Azure Front Door endpoint hosting App Configuration data (feature flags, etc.)
+ azureFrontDoorEndpoint: "YOUR-AZURE-FRONT-DOOR-ENDPOINT"
+};
\ No newline at end of file
diff --git a/client-app/src/index.jsx b/client-app/src/index.jsx
new file mode 100644
index 0000000..dc1ea2f
--- /dev/null
+++ b/client-app/src/index.jsx
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./App.css";
+
+window.addEventListener("beforeunload", (event) => {
+ // clear the localStorage when the user leaves the page
+ localStorage.clear()
+});
+
+const root = ReactDOM.createRoot(document.getElementById("root"));
+root.render(
+
+
+
+);
\ No newline at end of file
diff --git a/client-app/src/pages/AppContext.jsx b/client-app/src/pages/AppContext.jsx
new file mode 100644
index 0000000..083eccc
--- /dev/null
+++ b/client-app/src/pages/AppContext.jsx
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { config } from "../config";
+
+import { createContext, useState, useEffect } from "react";
+import { loadFromAzureFrontDoor } from "@azure/app-configuration-provider";
+import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "@microsoft/feature-management";
+import { createTelemetryPublisher } from "@microsoft/feature-management-applicationinsights-browser";
+import { ApplicationInsights } from "@microsoft/applicationinsights-web";
+
+export const AppContext = createContext();
+
+export const ContextProvider = ({ children }) => {
+ const [currentUser, setCurrentUser] = useState(undefined);
+ const [featureManager, setFeatureManager] = useState(undefined);
+ const appInsights = new ApplicationInsights({ config: {
+ connectionString: config.appInsightsConnectionString,
+ }});
+ appInsights.loadAppInsights();
+
+ useEffect(() => {
+ const init = async () => {
+ const appConfig = await loadFromAzureFrontDoor(
+ config.azureFrontDoorEndpoint,
+ {
+ featureFlagOptions: {
+ enabled: true
+ }
+ }
+ );
+
+ const fm = new FeatureManager(
+ new ConfigurationMapFeatureFlagProvider(appConfig),
+ {onFeatureEvaluated: createTelemetryPublisher(appInsights)}
+ );
+ setFeatureManager(fm);
+ };
+
+ init();
+ }, []);
+
+ const loginUser = (user) => {
+ setCurrentUser(user);
+ };
+
+ const logoutUser = () => {
+ setCurrentUser(undefined);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client-app/src/pages/Home.jsx b/client-app/src/pages/Home.jsx
new file mode 100644
index 0000000..56c79ef
--- /dev/null
+++ b/client-app/src/pages/Home.jsx
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { useState, useEffect, useContext } from "react";
+import { FaHeart, FaRegHeart } from "react-icons/fa";
+import { trackEvent } from "@microsoft/feature-management-applicationinsights-browser";
+import { AppContext } from "./AppContext";
+
+function Home() {
+ const { appInsights, appConfig, featureManager, currentUser, lastRefresh } = useContext(AppContext);
+ const [liked, setLiked] = useState(false);
+ const [variant, setVariant] = useState(undefined);
+
+ const quote = {
+ text: "The only way to do great work is to love what you do.",
+ author: "Steve Jobs"
+ };
+
+ useEffect(() => {
+ const init = async () => {
+ const variant = await featureManager?.getVariant("Greeting", { userId: currentUser });
+ setVariant(variant);
+ setLiked(false);
+ };
+
+ init();
+ }, [appConfig, featureManager, currentUser, lastRefresh]);
+
+ const handleLike = () => {
+ if (!liked) {
+ const targetingId = currentUser;
+ trackEvent(appInsights, targetingId, { name: "Like" });
+ }
+ setLiked(!liked);
+ };
+
+ return (
+
+ { variant ?
+ (
+ <>
+
+ { variant.name === "On" ?
+ ( <>Hi {currentUser ?? "Guest"}, hope this makes your day!> ) :
+ ( <>Quote of the day> ) }
+
+
+ "You cannot change what you are, only what you do."
+
+
+
+
+
+ >
+ )
+ :
Loading
+ }
+
+ );
+}
+
+export default Home;
\ No newline at end of file
diff --git a/client-app/src/pages/Login.jsx b/client-app/src/pages/Login.jsx
new file mode 100644
index 0000000..fdb3aad
--- /dev/null
+++ b/client-app/src/pages/Login.jsx
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { useState, useContext } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { AppContext } from "./AppContext";
+
+const Login = () => {
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [message, setMessage] = useState("");
+
+ const { loginUser } = useContext(AppContext);
+ const navigate = useNavigate();
+
+ const handleLogin = (e) => {
+
+ e.preventDefault();
+
+ // Retrieve user from localStorage
+ const users = JSON.parse(localStorage.getItem("users")) || [];
+ const user = users.find((user) => user.username === username && user.password === password);
+
+ if (user) {
+ loginUser(username);
+ navigate("/");
+ }
+ else {
+ setMessage("Invalid username or password!");
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client-app/src/pages/Register.jsx b/client-app/src/pages/Register.jsx
new file mode 100644
index 0000000..05d6fdf
--- /dev/null
+++ b/client-app/src/pages/Register.jsx
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { useState, useContext } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { AppContext } from "./AppContext";
+
+const Register = () => {
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [message, setMessage] = useState("");
+
+ const { loginUser } = useContext(AppContext);
+ const navigate = useNavigate();
+
+ const handleRegister = (e) => {
+
+ e.preventDefault();
+
+ const users = JSON.parse(localStorage.getItem("users")) || [];
+ const existingUser = users.some((user) => (user.username === username));
+
+ if (existingUser) {
+ setMessage("User already exists!");
+ }
+ else {
+ users.push({ username, password });
+ localStorage.setItem("users", JSON.stringify(users));
+ loginUser(username);
+ navigate("/");
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Register;
\ No newline at end of file
diff --git a/client-app/vite.config.js b/client-app/vite.config.js
new file mode 100644
index 0000000..ac6bd5b
--- /dev/null
+++ b/client-app/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite"
+import react from "@vitejs/plugin-react"
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()]
+})