Skip to content

Commit 3f3dd5f

Browse files
authored
Merge branch 'main' into MPI-116-FINAL-CollectAnonymousFeedback
2 parents 4ee3ea2 + ca453da commit 3f3dd5f

29 files changed

Lines changed: 1829 additions & 149 deletions

.github/workflows/firebase-deployment.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- name: Run Linting
3333
run: npm run lint
3434

35+
- name: Check TypeScript compilation
36+
run: npx tsc --noEmit
37+
3538
- name: Build Application
3639
run: npm run build
3740
env:

.github/workflows/lint.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Lint Validation
2+
3+
on:
4+
pull_request:
5+
branches: [main, master]
6+
push:
7+
branches: [main, master]
8+
9+
jobs:
10+
lint:
11+
name: Run ESLint
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: "18"
22+
cache: "npm"
23+
24+
- name: Install dependencies
25+
run: npm ci
26+
27+
- name: Run ESLint
28+
run: npm run lint
29+
30+
- name: Check TypeScript compilation
31+
run: npx tsc --noEmit

.github/workflows/playwright-tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ jobs:
2626
- name: Install dependencies
2727
run: npm ci
2828

29+
- name: Run ESLint
30+
run: npm run lint
31+
32+
- name: Check TypeScript compilation
33+
run: npx tsc --noEmit
34+
2935
- name: Install Playwright Browsers
3036
run: npx playwright install chromium
3137

README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,77 @@ Deploys when pushing a tag with the `prod-` prefix.
9191
2. View deployment: https://rainbowrelax.web.app
9292
3. Monitor status: **Actions > Deploy to Firebase PROD**
9393

94+
---
95+
# CI/CD Pipeline
96+
97+
This project uses GitHub Actions for automated testing, linting, and deployment. All workflows run on pull requests and pushes to ensure code quality.
98+
99+
## Workflows
100+
101+
### 1. Lint Validation (`lint.yml`)
102+
- **Triggers**: Pull requests and pushes to `main`
103+
- **Purpose**: Validates code quality before merge
104+
- **Runs**: ESLint and TypeScript compilation checks
105+
- **Duration**: ~2-3 minutes
106+
- **Benefit**: Prevents linting errors from reaching main branch, avoiding deployment failures
107+
108+
### 2. Playwright Tests (`playwright-tests.yml`)
109+
- **Triggers**: Pull requests and pushes to `main`
110+
- **Purpose**: Run end-to-end tests to ensure functionality
111+
- **Runs**:
112+
- ESLint validation
113+
- TypeScript compilation
114+
- Build application
115+
- Run all 84+ E2E tests (using 4 parallel workers)
116+
- **Duration**: ~5-7 minutes
117+
- **Benefit**: Comprehensive validation before merge and deployment
118+
119+
### 3. Firebase Deployment Workflows
120+
121+
All deployment workflows use the reusable `firebase-deployment.yml` workflow which ensures:
122+
1. ✅ ESLint validation passes
123+
2. ✅ TypeScript compilation succeeds
124+
3. ✅ Application builds successfully
125+
4. ✅ Only validated code reaches production
126+
127+
#### Development Deployment (`firebase-dev.yml`)
128+
- **Triggers**: Push to `main` branch
129+
- **Includes**: Full validation (linting, TypeScript, build)
130+
- **Deploy**: Automatically to `rainbowrelax-dev` site
131+
- **URL**: https://rainbowrelax-dev.web.app
132+
133+
#### QA Deployment (`firebase-qa.yml`)
134+
- **Triggers**: Push tag matching `qa-*` pattern
135+
- **Includes**: Full validation before deployment
136+
- **Deploy**: To `rainbowrelax-qa` site
137+
- **URL**: https://rainbowrelax-qa.web.app
138+
139+
#### Production Deployment (`firebase-prod.yml`)
140+
- **Triggers**: Push tag matching `prod-*` pattern
141+
- **Includes**: Full validation before deployment
142+
- **Deploy**: To `rainbowrelax` site
143+
- **URL**: https://rainbowrelax.web.app
144+
145+
## Workflow Integration
146+
147+
The CI/CD pipeline ensures that:
148+
- **Pull Requests**: Lint validation and tests run automatically
149+
- **Main Branch**: All validations pass before any deployment
150+
- **Deployments**: Only validated code reaches DEV, QA, and PROD environments
151+
152+
This prevents duplicate PRs caused by linting failures and ensures only tested, validated code is deployed.
153+
154+
## Local Development
155+
156+
Before pushing, run locally to catch issues early:
157+
158+
```bash
159+
npm run lint # Check for linting errors
160+
npx tsc --noEmit # Verify TypeScript compilation
161+
npm run build # Verify build works
162+
npm run test:e2e # Run all tests
163+
```
164+
94165
---
95166
# Audio Credits
96167

src/App.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AppRoutes } from "./router/routes";
44
import { init } from "./i18n/init";
55
import { MainAnimationProvider } from "./context/MainAnimationProvider";
66
import { AudioProvider } from "./context/AudioProvider";
7+
import { WidgetConfigProvider } from "./context/WidgetConfigProvider";
78

89

910
init();
@@ -31,13 +32,15 @@ function App() {
3132
const basePath = import.meta.env.BASE_URL.replace(/\/$/, "");
3233

3334
return (
34-
<AudioProvider>
35-
<MainAnimationProvider>
36-
<Router basename={basePath}>
37-
<AppContent />
38-
</Router>
39-
</MainAnimationProvider>
40-
</AudioProvider>
35+
<WidgetConfigProvider>
36+
<AudioProvider>
37+
<MainAnimationProvider>
38+
<Router basename={basePath}>
39+
<AppContent />
40+
</Router>
41+
</MainAnimationProvider>
42+
</AudioProvider>
43+
</WidgetConfigProvider>
4144
);
4245
}
4346

src/components/Instructions.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default function BreathingInstructions({
7373
useEffect(() => {
7474
setBackgroundMusic(isSoundEnabled && shouldPlayMusic);
7575
setGuidedVoice(isSoundEnabled && showIntro);
76-
}, [isSoundEnabled, shouldPlayMusic, setBackgroundMusic]);
76+
}, [isSoundEnabled, shouldPlayMusic, setBackgroundMusic, setGuidedVoice, showIntro]);
7777

7878
useEffect(() => {
7979
if (timeLeft === 0 && !showIntro && !exerciseCompleted) {
@@ -89,7 +89,7 @@ export default function BreathingInstructions({
8989
navigate("/thank-you");
9090
resetAnimation();
9191
}
92-
}, [timeLeft, showIntro, exerciseCompleted, navigate]);
92+
}, [timeLeft, showIntro, exerciseCompleted, navigate, resetAnimation]);
9393

9494
useEffect(() => {
9595
if (hasResetRef.current) return;
@@ -118,7 +118,7 @@ export default function BreathingInstructions({
118118
);
119119
hasResetRef.current = false;
120120
};
121-
}, []);
121+
}, [resetAnimation, resetExercise]);
122122

123123
useEffect(() => {
124124
if (!animationSet.waitSet) {

src/components/Logo.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next';
3-
4-
import TrevorLogoEn from '../assets/TrevorLogo-en.svg';
5-
import TrevorLogoEs from '../assets/TrevorLogo-es.svg';
3+
import { useWidgetConfig } from '../context/WidgetConfigContext';
64

75

86
interface LogoProps {
@@ -19,16 +17,19 @@ const LOGO_STYLE: React.CSSProperties = {
1917
};
2018

2119
const Logo: React.FC<LogoProps> = ({ className }) => {
22-
const { t, i18n } = useTranslation();
23-
const language = i18n.language.startsWith('es') ? 'es' : 'en';
24-
const logoSrc = language === 'es' ? TrevorLogoEs : TrevorLogoEn;
20+
const { t } = useTranslation();
21+
const { config, logoSrc } = useWidgetConfig();
2522

2623
return (
2724
<img
2825
src={logoSrc}
2926
alt={t('LogoAlt')}
3027
style={LOGO_STYLE}
3128
className={className}
29+
onError={() => {
30+
console.error('Failed to load CDN logo:', config.logoUrl);
31+
// Fallback is handled by the src attribute change
32+
}}
3233
/>
3334
);
3435
};

src/components/MainAnimation.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { motion, useAnimation } from "framer-motion";
2-
import { MainAnimationObject } from "../context/animationObjects";
2+
import { MainAnimationObject, CircleProps } from "../context/animationObjects";
33
import { BreathingExerciseFactory } from "../utils/breathingExerciseFactory";
44
import { useBreathingPhases } from "../hooks/useBreathingPhases";
55
import { useEffect } from "react";
@@ -10,12 +10,6 @@ interface MainAnimationProps {
1010
}
1111

1212
export const MainAnimation = ({ animation, isPaused }: MainAnimationProps) => {
13-
const positionTimes = {
14-
top: { duration: 3 },
15-
left: { duration: 3 },
16-
right: { duration: 3 },
17-
bottom: { duration: 3 },
18-
};
1913
const exercise = BreathingExerciseFactory.getExercise("4-7-8");
2014
const isCycle = animation.firstCircle.duration === exercise.cycleDuration;
2115
const inhaleTime = isCycle ? exercise.instructions[0].duration : 0;
@@ -58,7 +52,13 @@ export const MainAnimation = ({ animation, isPaused }: MainAnimationProps) => {
5852

5953
useEffect(() => {
6054
if (isCycle) return;
61-
const start = (controls: any, props: any) => {
55+
const positionTimes = {
56+
top: { duration: 3 },
57+
left: { duration: 3 },
58+
right: { duration: 3 },
59+
bottom: { duration: 3 },
60+
};
61+
const start = (controls: ReturnType<typeof useAnimation>, props: CircleProps) => {
6262
controls.start({
6363
scale: props.scale,
6464
...props.position,
@@ -89,7 +89,7 @@ export const MainAnimation = ({ animation, isPaused }: MainAnimationProps) => {
8989
thirdControls.stop();
9090
fourthControls.stop();
9191
};
92-
}, [animation, isPaused, isCycle]);
92+
}, [animation, isPaused, isCycle, firstControls, secondControls, thirdControls, fourthControls]);
9393

9494

9595

src/components/NavBar.tsx

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,57 @@
1-
import Logo from "./Logo";
2-
import { useTranslation } from "react-i18next";
3-
import ToggleButton from "./ToggleButton";
4-
5-
6-
const NavBar = () => {
7-
const { t } = useTranslation();
8-
9-
const donateUrl = t("donate-url");
10-
const homepageUrl = t("homepage-url");
11-
12-
return (
13-
<div className="fixed flex items-center justify-between w-full px-4 md:px-8 py-4">
14-
<div
15-
className="flex items-center cursor-pointer"
16-
onClick={() =>
17-
(window.location.href = homepageUrl)
18-
}
19-
>
20-
<Logo className="Logo" />
21-
</div>
22-
23-
24-
25-
<div className="flex items-center space-x-4">
26-
{/* Language toggle button*/}
27-
<ToggleButton />
28-
29-
<a
30-
href={donateUrl} target="_blank" rel="noopener"
31-
className="flex px-6 sm:px-6 py-2 sm:py-2 text-[var(--color-button-text)] bg-[var(--color-button)] rounded-md shadow-md hover:opacity-80 text-sm sm:text-base max-w-[5.5rem] items-center justify-center"
32-
>
33-
<p className="text-[--font-global] text-[15px] font-bold">{t("Donate")}</p>
34-
</a>
35-
36-
</div>
37-
</div>
38-
);
39-
};
40-
41-
export default NavBar;
1+
import Logo from "./Logo";
2+
import { useTranslation } from "react-i18next";
3+
import ToggleButton from "./ToggleButton";
4+
import { useWidgetConfig } from "../context/WidgetConfigContext";
5+
6+
7+
const NavBar = () => {
8+
const { t } = useTranslation();
9+
const { config } = useWidgetConfig();
10+
11+
const donateUrl = config.donationUrl;
12+
const homepageUrl = config.homeUrl;
13+
const helpUrl = config.helpUrl;
14+
15+
return (
16+
<div className="fixed flex items-center justify-between w-full px-4 md:px-8 py-4">
17+
<div
18+
className="flex items-center cursor-pointer"
19+
onClick={() =>
20+
homepageUrl && (window.location.href = homepageUrl)
21+
}
22+
>
23+
<Logo className="Logo" />
24+
</div>
25+
26+
27+
28+
<div className="flex items-center space-x-4">
29+
{/* Language toggle button*/}
30+
<ToggleButton />
31+
32+
{/* Donate button - show if URL is provided */}
33+
{donateUrl && (
34+
<a
35+
href={donateUrl} target="_blank" rel="noopener"
36+
className="flex px-6 sm:px-6 py-2 sm:py-2 text-[var(--color-button-text)] bg-[var(--color-button)] rounded-md shadow-md hover:opacity-80 text-sm sm:text-base max-w-[5.5rem] items-center justify-center"
37+
>
38+
<p className="text-[--font-global] text-[15px] font-bold">{t("Donate")}</p>
39+
</a>
40+
)}
41+
42+
{/* Help button - show if URL is provided */}
43+
{helpUrl && (
44+
<a
45+
href={helpUrl} target="_blank" rel="noopener"
46+
className="flex px-6 sm:px-6 py-2 sm:py-2 text-[var(--color-button-text)] bg-[var(--color-button)] rounded-md shadow-md hover:opacity-80 text-sm sm:text-base max-w-[5.5rem] items-center justify-center"
47+
>
48+
<p className="text-[--font-global] text-[15px] font-bold">{t("Help")}</p>
49+
</a>
50+
)}
51+
52+
</div>
53+
</div>
54+
);
55+
};
56+
57+
export default NavBar;

0 commit comments

Comments
 (0)