From caf6222d1d6de9ff59dae22e26ea348af82f9cdf Mon Sep 17 00:00:00 2001 From: Ian Monroe Date: Tue, 26 May 2026 14:51:28 +0000 Subject: [PATCH 1/7] IFDM-153: Set up a staging site --- docs/deployment.md | 107 ++++++++++++++++++++++++++++++++-------- next.config.ts | 2 +- package.json | 4 +- scripts/check-branch.js | 7 +-- 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index caa38ff..a130a2f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,43 +1,110 @@ -# Instructions for deployment and embedding +# Instructions for deployment and embedding -## Deployment +## Environments -The app deploys to https://ifdm-learning.stanford.edu/ via GitHub Pages and the `gh-pages` branch. The deploy script will automatically run the build step and generate the static files based on your local `1.x` branch code. Make sure you are on the latest `1.x` branch before deploying. +| Environment | Branch | URL | Command | +|---|---|---|---| +| Production | `1.x` | https://ifdm-learning.stanford.edu/ | `yarn deploy` | +| Staging | `dev` | https://su-sws.github.io/ifdm_learning_apps_staging/ | `yarn deploy:staging` | -### Deployment Steps +--- + +## Development workflow -1. Ensure you are on the latest `1.x` branch code: - - `git checkout 1.x && git fetch && git pull` -2. Make sure you are using the correct Node version: - - If you use nvm and are not already on the correct version, run: `nvm use` -3. (Optional) Test the build locally before deploying: - - `yarn build` -4. Deploy to GitHub Pages: - - `yarn deploy` -5. After deploying, verify the deployment: - - Visit https://github.com/SU-SWS/ifdm_learning_apps/deployments to confirm the deployment ran successfully. +Feature branches are developed and reviewed via pull request, then merged in two stages: + +``` +feature/my-branch → PR → dev → yarn deploy:staging → QA on staging + ↓ approved + PR to 1.x → yarn deploy → production +``` + +1. Open a PR from your feature branch targeting `dev` +2. After merge, deploy to staging and verify: `yarn deploy:staging` +3. Once staging is confirmed, open a PR from `dev` into `1.x` +4. After merge, deploy to production: `yarn deploy` --- -### Embedding a Calculator +## Deploying to production + +The deploy script enforces that you are on the `1.x` branch before building. + +1. Switch to the latest `1.x`: + ``` + git checkout 1.x && git fetch && git pull + ``` +2. Confirm the correct Node version is active: + ``` + nvm use + ``` +3. Deploy: + ``` + yarn deploy + ``` +4. Verify at https://github.com/SU-SWS/ifdm_learning_apps/deployments + +--- + +## Deploying to staging + +The staging deploy script enforces that you are on the `dev` branch before building. + +1. Switch to the latest `dev`: + ``` + git checkout dev && git fetch && git pull + ``` +2. Confirm the correct Node version is active: + ``` + nvm use + ``` +3. Deploy: + ``` + yarn deploy:staging + ``` +4. Verify at https://github.com/SU-SWS/ifdm_learning_apps_staging/deployments and preview at https://su-sws.github.io/ifdm_learning_apps_staging/ + +--- + +## One-time staging setup (already done — for reference) + +If the staging environment ever needs to be rebuilt from scratch: + +1. Create a new GitHub repo: `SU-SWS/ifdm_learning_apps_staging` +2. In that repo's Settings → Pages, set the source to the `gh-pages` branch +3. Add the staging remote to your local clone: + ``` + git remote add staging git@github.com:SU-SWS/ifdm_learning_apps_staging.git + ``` + Note: `yarn deploy:staging` targets this remote directly via `gh-pages -r`; the local remote entry is for reference and manual pushes only. + +--- + +## Embedding a calculator Example iFrame: -```angular2html +```html ``` + See below screencast as an example of embedding the iframe: ![Embedding Example](images/embedding-in-mighty.gif) +--- + ## Troubleshooting -### Changes Not Appearing +### Changes not appearing 1. Clear browser cache -2. Check GitHub Pages deployment status -3. Verify the gh-pages branch updated +2. Check GitHub Pages deployment status in the repo's Deployments tab +3. Verify the `gh-pages` branch updated with a recent commit + +### Staging assets not loading (404 on `/_next/` paths) +The staging build sets `basePath=/ifdm_learning_apps_staging` so all asset paths are prefixed correctly for the project URL. If you see 404s on static assets, confirm `build:staging` was used (not the plain `build` command). -### iframe Issues +### iframe issues - Ensure the source URL is correct - Check for CORS restrictions - Test the iframe URL directly in browser diff --git a/next.config.ts b/next.config.ts index 36a15be..520da8c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: 'export', trailingSlash: true, - basePath: '', + basePath: process.env.NEXT_BASE_PATH || '', eslint: { // Directories to run ESLint on during builds dirs: ['app'], diff --git a/package.json b/package.json index aabc014..f4417f4 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "dev": "next dev --turbopack", "build:local": "next build", "build": "echo \" \" > public/.nojekyll && next build && cp CNAME out/CNAME", - "predeploy": "node scripts/check-branch.js && yarn build", + "build:staging": "echo \" \" > public/.nojekyll && NEXT_BASE_PATH=/ifdm_learning_apps_staging next build", + "predeploy": "node scripts/check-branch.js 1.x && yarn build", "deploy": "gh-pages -d out", + "deploy:staging": "node scripts/check-branch.js dev && yarn build:staging && gh-pages -d out -r git@github.com:SU-SWS/ifdm_learning_apps_staging.git", "start": "next start", "lint": "next lint" }, diff --git a/scripts/check-branch.js b/scripts/check-branch.js index 9e40988..7f87ec1 100644 --- a/scripts/check-branch.js +++ b/scripts/check-branch.js @@ -1,11 +1,12 @@ const { execSync } = require('child_process'); +const allowedBranch = process.argv[2] || '1.x'; + try { const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); - if (branch !== '1.x') { - console.error(`Build blocked on all branches but 1.x`); - console.error('Builds are not allowed on this branch.'); + if (branch !== allowedBranch) { + console.error(`Deploy blocked: must be on branch '${allowedBranch}' (currently on '${branch}').`); process.exit(1); } } catch (error) { From 1478c0e625a5b5dd2c7bcd153438b261e3bcf8d8 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Fri, 5 Jun 2026 16:06:19 -0700 Subject: [PATCH 2/7] Client feedback changes --- .../present-value-calculator-v2/page.tsx | 264 +++++++++++------- 1 file changed, 163 insertions(+), 101 deletions(-) diff --git a/app/interactives/present-value-calculator-v2/page.tsx b/app/interactives/present-value-calculator-v2/page.tsx index a0b0362..d539a8f 100644 --- a/app/interactives/present-value-calculator-v2/page.tsx +++ b/app/interactives/present-value-calculator-v2/page.tsx @@ -13,6 +13,8 @@ import { SelectValue, } from "@/app/ui/components/select" import ThemeToggle from "@/app/lib/theme-toggle" +import { FaCircleInfo } from "react-icons/fa6" + type CompoundingFrequency = "annually" | "semi-annually" | "quarterly" | "monthly" | "biweekly" | "weekly" | "daily" @@ -28,19 +30,19 @@ const frequencyMap: Record("") + const [interestRate, setInterestRate] = useState("") + const [timePeriod, setTimePeriod] = useState("") const [compoundingFrequency, setCompoundingFrequency] = useState("annually") // Payment Series State - const [paymentAmount, setPaymentAmount] = useState(0) - const [paymentInterestRate, setPaymentInterestRate] = useState(0) - const [numberOfPayments, setNumberOfPayments] = useState(0) + const [paymentAmount, setPaymentAmount] = useState("") + const [paymentInterestRate, setPaymentInterestRate] = useState("") + const [numberOfPayments, setNumberOfPayments] = useState("") const [paymentFrequency, setPaymentFrequency] = useState("annually") - const [finalAmount, setFinalAmount] = useState(0) + const [finalAmount, setFinalAmount] = useState("") // Error states const [futureValueError, setFutureValueError] = useState(""); @@ -48,17 +50,17 @@ export default function PresentValueCalculator() { const [timePeriodError, setTimePeriodError] = useState(""); const [paymentAmountError, setPaymentAmountError] = useState(""); const [finalAmountError, setFinalAmountError] = useState(""); - const [paymentInterestRateError, setPaymentInterestRateError] = - useState(""); + const [paymentInterestRateError, setPaymentInterestRateError] = useState(""); const [numberOfPaymentsError, setNumberOfPaymentsError] = useState(""); const singleCalculations = useMemo(() => { - const rate = interestRate / 100 + const fv = parseFloat(futureValue) || 0 + const rate = (parseFloat(interestRate) || 0) / 100 const n = frequencyMap[compoundingFrequency].periods - const totalPeriods = timePeriod // CHANGED: use input directly as total periods + const totalPeriods = parseFloat(timePeriod) || 0 const periodRate = rate / n - const presentValue = futureValue / Math.pow(1 + periodRate, totalPeriods) - const discountAmount = futureValue - presentValue + const presentValue = fv / Math.pow(1 + periodRate, totalPeriods) + const discountAmount = fv - presentValue return { presentValue, @@ -68,25 +70,23 @@ export default function PresentValueCalculator() { }, [futureValue, interestRate, timePeriod, compoundingFrequency]) const paymentCalculations = useMemo(() => { - const rate = paymentInterestRate / 100; + const rate = (parseFloat(paymentInterestRate) || 0) / 100; const n = frequencyMap[paymentFrequency].periods; + const periods = parseFloat(numberOfPayments) || 0; + const pa = parseFloat(paymentAmount) || 0; + const fa = parseFloat(finalAmount) || 0; const periodRate = rate / n; let pvPayments: number; if (periodRate === 0) { - pvPayments = paymentAmount * numberOfPayments; + pvPayments = pa * periods; } else { - pvPayments = - paymentAmount * - ((1 - Math.pow(1 + periodRate, -numberOfPayments)) / periodRate); + pvPayments = pa * ((1 - Math.pow(1 + periodRate, -periods)) / periodRate); } - // PV of the lump sum final amount discounted over all periods - const pvFinalAmount = - finalAmount / Math.pow(1 + periodRate, numberOfPayments); - + const pvFinalAmount = fa / Math.pow(1 + periodRate, periods); const presentValue = pvPayments + pvFinalAmount; - const totalPayments = paymentAmount * numberOfPayments + finalAmount; + const totalPayments = pa * periods + fa; const discountAmount = totalPayments - presentValue; return { @@ -94,13 +94,7 @@ export default function PresentValueCalculator() { totalPayments, discountAmount, }; - }, [ - paymentAmount, - finalAmount, - paymentInterestRate, - numberOfPayments, - paymentFrequency, - ]); + }, [paymentAmount, finalAmount, paymentInterestRate, numberOfPayments, paymentFrequency]); const formatCurrency = (value: number) => { return new Intl.NumberFormat("en-US", { @@ -114,7 +108,6 @@ export default function PresentValueCalculator() { return (
- {/* Header */}

Present Value Calculator

@@ -132,10 +125,11 @@ export default function PresentValueCalculator() {
- {/* Future Value Input */}

Enter a future value to calculate what it is worth today.

+ + {/* Future Value */}
- {/* Interest Rate Input */} + {/* Interest Rate */}
@@ -223,7 +233,7 @@ export default function PresentValueCalculator() { )}
- {/* Time Period Input */} + {/* Time Period */}
- {/* Results Section */} + {/* Results */}
- {/* Main Present Value Display */}

Present value

{formatCurrency(singleCalculations.presentValue)}

- - {/* Breakdown */}
- {formatCurrency(futureValue)} + {formatCurrency(parseFloat(futureValue) || 0)}
@@ -333,18 +350,19 @@ export default function PresentValueCalculator() {
- {/* Payment Amount Input */}

- Find what a series of payments is worth today. Enter a - payment amount and number of payments to calculate the - present value. + Find what a series of payments is worth today. Enter the + payment per period and number of periods (payments) to + calculate the present value.

+ + {/* Payment Amount */}
{ - const val = - e.target.value === "" ? 0 : Number(e.target.value); + const raw = e.target.value; + const val = Number(raw); if (val < 0) { setPaymentAmountError( "Payment amount must be greater than 0.", @@ -368,9 +386,14 @@ export default function PresentValueCalculator() { return; } setPaymentAmountError(""); - setPaymentAmount(val); + setPaymentAmount(raw); }} - className={`border-1 w-full rounded-md shadow-sm py-2 px-3 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${paymentAmount > 0 ? "pl-7" : "pl-8"} ${paymentAmountError ? "border-[var(--color-inline-error)] border-2" : ""}`} + onBlur={(e) => { + const val = parseFloat(e.target.value); + if (!isNaN(val)) setPaymentAmount(String(val)); + else setPaymentAmount(""); + }} + className={`border-1 w-full rounded-md shadow-sm py-2 px-3 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${parseFloat(paymentAmount) > 0 ? "pl-7" : "pl-8"} ${paymentAmountError ? "border-[var(--color-inline-error)] border-2" : ""}`} />
{paymentAmountError && ( @@ -383,14 +406,33 @@ export default function PresentValueCalculator() { )}
- {/* Final Amount Input */} + {/* Final Amount */}
- +
+ +
+ + +
+
{finalAmountError && ( @@ -428,7 +479,7 @@ export default function PresentValueCalculator() { )}
- {/* Interest Rate Input */} + {/* Payment Interest Rate */}
- {/* Number of Payments Input */} + {/* Number of Payments */}
{ - const val = - e.target.value === "" ? 0 : Number(e.target.value); - if (val < 1) { + const raw = e.target.value; + const val = Number(raw); + if (val < 0) { setNumberOfPaymentsError( "Number of payments must be greater than 0.", ); return; } setNumberOfPaymentsError(""); - setNumberOfPayments(val); + setNumberOfPayments(raw); + }} + onBlur={(e) => { + const val = parseFloat(e.target.value); + if (!isNaN(val)) setNumberOfPayments(String(val)); + else setNumberOfPayments(""); }} className={`border-1 w-full rounded-md shadow-sm py-2 px-3 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${numberOfPaymentsError ? "border-[var(--color-inline-error)] border-2" : ""}`} - min={1} + min={0} max={1000} step={1} /> @@ -521,7 +580,7 @@ export default function PresentValueCalculator() { {/* Payment Frequency */}
-
- {/* Results Section */} + {/* Results */}

Present value @@ -554,8 +618,6 @@ export default function PresentValueCalculator() {

{formatCurrency(paymentCalculations.presentValue)}

- - {/* Breakdown */}
From b01395d850eae6e6312918475d066481af4a68b4 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Mon, 8 Jun 2026 13:22:55 -0700 Subject: [PATCH 3/7] fixup --- .../present-value-calculator-v2/page.tsx | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/app/interactives/present-value-calculator-v2/page.tsx b/app/interactives/present-value-calculator-v2/page.tsx index d539a8f..64fc670 100644 --- a/app/interactives/present-value-calculator-v2/page.tsx +++ b/app/interactives/present-value-calculator-v2/page.tsx @@ -4,6 +4,7 @@ import { useState, useMemo } from "react" import { Input } from "@/app/ui/components/input" import { Label } from "@/app/ui/components/label" +import { Button } from "@/app/ui/components/button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/ui/components/tabs" import { Select, @@ -53,6 +54,28 @@ export default function PresentValueCalculator() { const [paymentInterestRateError, setPaymentInterestRateError] = useState(""); const [numberOfPaymentsError, setNumberOfPaymentsError] = useState(""); + const resetSingle = () => { + setFutureValue(""); + setInterestRate(""); + setTimePeriod(""); + setCompoundingFrequency("annually"); + setFutureValueError(""); + setInterestRateError(""); + setTimePeriodError(""); + } + + const resetSeries = () => { + setPaymentAmount(""); + setPaymentInterestRate(""); + setNumberOfPayments(""); + setPaymentFrequency("annually"); + setFinalAmount(""); + setPaymentAmountError(""); + setPaymentInterestRateError(""); + setNumberOfPaymentsError(""); + setFinalAmountError(""); + } + const singleCalculations = useMemo(() => { const fv = parseFloat(futureValue) || 0 const rate = (parseFloat(interestRate) || 0) / 100 @@ -158,6 +181,12 @@ export default function PresentValueCalculator() { ); return; } + if (val > 1000000000) { + setFutureValueError( + "Future value cannot exceed 1,000,000,000.", + ); + return; + } setFutureValueError(""); setFutureValue(raw); }} @@ -167,6 +196,8 @@ export default function PresentValueCalculator() { else setFutureValue(""); }} className={`border-1 w-full rounded-md shadow-sm py-2 px-3 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${parseFloat(futureValue) > 0 ? "pl-7" : "pl-8"} ${futureValueError ? "border-[var(--color-inline-error)] border-2" : ""}`} + min={0} + max={1000000000} />
{futureValueError && ( @@ -197,9 +228,9 @@ export default function PresentValueCalculator() { onChange={(e) => { const raw = e.target.value; const val = Number(raw); - if (val > 100) { + if (val > 1000) { setInterestRateError( - "Annual interest rate cannot exceed 100%.", + "Annual interest rate cannot exceed 1000%.", ); } else { setInterestRateError(""); @@ -214,7 +245,7 @@ export default function PresentValueCalculator() { className={`border-1 w-full rounded-md shadow-sm py-2 px-3 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${interestRateError ? "border-[var(--color-inline-error)] border-2" : ""}`} min={0} max={100} - step={0.1} + step={0.01} />
@@ -497,7 +537,7 @@ export default function PresentValueCalculator() { onChange={(e) => { const raw = e.target.value; const val = Number(raw); - if (val > 100) { + if (val > 1000) { setPaymentInterestRateError( "Annual interest rate cannot exceed 100%.", ); @@ -514,7 +554,7 @@ export default function PresentValueCalculator() { className={`border-1 w-full rounded-md shadow-sm py-2 px-3 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${paymentInterestRateError ? "border-[var(--color-inline-error)] border-2" : ""}`} min={0} max={100} - step={0.1} + step={0.01} />

From 8e0f34f62e1dd4218b3f3e31acc15dd4e3ab801a Mon Sep 17 00:00:00 2001 From: Ian Monroe Date: Tue, 9 Jun 2026 15:32:57 +0000 Subject: [PATCH 4/7] Updated yarn script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f4417f4..5edc097 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "build": "echo \" \" > public/.nojekyll && next build && cp CNAME out/CNAME", "build:staging": "echo \" \" > public/.nojekyll && NEXT_BASE_PATH=/ifdm_learning_apps_staging next build", "predeploy": "node scripts/check-branch.js 1.x && yarn build", - "deploy": "gh-pages -d out", - "deploy:staging": "node scripts/check-branch.js dev && yarn build:staging && gh-pages -d out -r git@github.com:SU-SWS/ifdm_learning_apps_staging.git", + "deploy": "gh-pages -d out --dotfiles", + "deploy:staging": "node scripts/check-branch.js dev && yarn build:staging && gh-pages -d out --dotfiles -r git@github.com:SU-SWS/ifdm_learning_apps_staging.git", "start": "next start", "lint": "next lint" }, From fc5a850302d9186d5855997fc3b76d50ce48798c Mon Sep 17 00:00:00 2001 From: Ian Monroe Date: Tue, 9 Jun 2026 15:40:19 +0000 Subject: [PATCH 5/7] Updated documentation --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94b3191..8d479c4 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,32 @@ This repository serves as source/hosting for an interactive learning tool. The g --- -## Deployment +## Environments -Please see documentation on deployment and embedding here: [deployment.md](docs/deployment.md) +| Environment | URL | Branch | Deploy command | +|---|---|---|---| +| Production | https://ifdm-learning.stanford.edu/ | `1.x` | `yarn deploy` | +| Staging | https://su-sws.github.io/ifdm_learning_apps_staging/ | `dev` | `yarn deploy:staging` | + +### Development workflow + +New features are developed on feature branches, reviewed via pull request, then deployed to staging for QA before going to production: + +``` +feature/my-branch → PR into dev → yarn deploy:staging → review on staging + ↓ approved + PR into 1.x → yarn deploy → production +``` + +1. Create a feature branch off `dev` (or `1.x` for urgent fixes) +2. Open a PR targeting `dev` +3. After merge, deploy to staging and verify: `git checkout dev && git pull && yarn deploy:staging` +4. Once staging looks good, open a PR from `dev` into `1.x` +5. After merge, deploy to production: `git checkout 1.x && git pull && yarn deploy` + +The deploy scripts enforce branch — `yarn deploy` will fail if you're not on `1.x`, and `yarn deploy:staging` will fail if you're not on `dev`. + +For full deployment details and troubleshooting, see [docs/deployment.md](docs/deployment.md). ## Local Setup From a696c8745a3ce13e630ea652bd3770f9607e2cc0 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Tue, 9 Jun 2026 08:58:48 -0700 Subject: [PATCH 6/7] fixup --- app/interactives/present-value-calculator-v2/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/interactives/present-value-calculator-v2/page.tsx b/app/interactives/present-value-calculator-v2/page.tsx index 64fc670..4875d93 100644 --- a/app/interactives/present-value-calculator-v2/page.tsx +++ b/app/interactives/present-value-calculator-v2/page.tsx @@ -468,7 +468,7 @@ export default function PresentValueCalculator() { role="tooltip" className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 w-56 rounded-md bg-[var(--info-popup-background)] border-1 border-grey-border text-xs p-4 invisible group-hover:visible group-focus-within:visible opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity pointer-events-none z-10" > - A lump sum recieved or paid at the end of the payment + A lump sum received or paid at the end of the payment series (also called final value or future value).
From 61b8e75d3bfd1f4763145bcf4cd1c479676b6d94 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Tue, 9 Jun 2026 09:41:39 -0700 Subject: [PATCH 7/7] Testing dev branch flow --- app/page.tsx | 83 ++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 6cfe203..845d3ad 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,8 +2,7 @@ import fs from 'fs'; import path from 'path'; import Link from 'next/link'; -import React from 'react'; -import "@/app/ui/globals.css"; +import React from 'react'; // Function to get all page routes from the app directory function getPageRoutes() { @@ -48,52 +47,46 @@ export default function HomePage() { const routes = getPageRoutes(); return ( -
-
-

- Welcome to The IFDM Lessons and Games -

-
-

- This page helps developers find the pages they are working on quicker. -

-
+
+
+

+ Welcome to The IFDM Lessons and Games +

-
-

- Available Pages -

+
+

+ Available Pages +

- {routes.length > 0 ? ( -
- {routes.map((route) => ( - -

- {route.name} -

-

- {route.path} -

- - ))} -
- ) : ( -

- No pages found. Create some pages in the app directory! -

- )} -
+ {routes.length > 0 ? ( +
+ {routes.map((route) => ( + +

+ {route.name} +

+

{route.path}

+ + ))} +
+ ) : ( +

+ No pages found. Create some pages in the app directory! +

+ )} +
-
-

- This page automatically updates when you add new pages to your app directory. -

-
-
+
+

+ This page automatically updates when you add new pages to your app + directory. +

+
+
); }