Skip to content

Commit 216aee9

Browse files
authored
Merge branch 'main' into MPI-116-FINAL-CollectAnonymousFeedback
2 parents 3f3dd5f + 3999946 commit 216aee9

19 files changed

Lines changed: 698 additions & 95 deletions

README.md

Lines changed: 209 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To run project in development enviroment:
2727

2828
When embedding the Rainbow Relax application in another site (e.g., via an `<iframe>`), you can control specific behaviors via URL parameters.
2929

30-
## `showquickescape` (optional)
30+
## Quick Escape (`showquickescape`)
3131

3232
- **Type:** `boolean` (`true` or `false`)
3333
- **Default:** `false`
@@ -39,6 +39,161 @@ When embedding the Rainbow Relax application in another site (e.g., via an `<ifr
3939
<iframe src="https://trevorproject.github.io/rainbow-relax/dev/?showquickescape=true" width="100%" height="600"></iframe>
4040
```
4141

42+
## Asset Customization
43+
44+
### Logo (`logoUrl`)
45+
46+
- **Type:** `string` (URL)
47+
- **Default:** Trevor Project logo (language-specific)
48+
- **Description:** Replace the default Trevor Project logo with your organization's logo.
49+
- **Requirements:**
50+
- Must be HTTPS URL
51+
- Recommended: Square aspect ratio (1:1)
52+
- Recommended size: 150x150px to 300x300px
53+
- Supported formats: SVG, PNG, JPG, WebP
54+
55+
### Audio Files
56+
57+
#### Background Audio (`backgroundUrl`)
58+
59+
- **Type:** `string` (URL)
60+
- **Default:** Default ambient background audio
61+
- **Description:** Custom background audio that plays during the breathing exercise.
62+
- **Requirements:**
63+
- Must be HTTPS URL
64+
- Supported formats: MP3, WAV, OGG
65+
- Recommended: 2-5 minute loopable audio
66+
67+
#### Instructions Audio (`instructionsUrl`)
68+
69+
- **Type:** `string` (URL)
70+
- **Default:** Default instruction audio
71+
- **Description:** Custom audio for breathing exercise instructions.
72+
- **Requirements:**
73+
- Must be HTTPS URL
74+
- Supported formats: MP3, WAV, OGG
75+
- Recommended: Clear, calm voice
76+
77+
#### Guided Voice (`guidedVoiceUrl`)
78+
79+
- **Type:** `string` (URL)
80+
- **Default:** Default guided voice
81+
- **Description:** Custom guided voice for the breathing exercise.
82+
- **Requirements:**
83+
- Must be HTTPS URL
84+
- Supported formats: MP3, WAV, OGG
85+
- Recommended: Calm, soothing voice
86+
87+
#### Breathing Cycle Audio (`audioUrl`)
88+
89+
- **Type:** `string` (URL)
90+
- **Default:** Default breathing cycle audio
91+
- **Description:** Custom audio for the breathing cycle (inhale, hold, exhale).
92+
- **Requirements:**
93+
- Must be HTTPS URL
94+
- Supported formats: MP3, WAV, OGG
95+
- Recommended: Subtle, non-distracting audio
96+
97+
## Navigation Customization
98+
99+
### Donation Button (`donationUrl`)
100+
101+
- **Type:** `string` (URL) or `"no"`
102+
- **Default:** Trevor Project donation page
103+
- **Description:** Custom donation page URL or hide the donation button entirely.
104+
- **Special Values:**
105+
- `"no"` - Hides the donation button
106+
- Any valid URL - Redirects to custom donation page
107+
108+
### Help Button (`helpUrl`)
109+
110+
- **Type:** `string` (URL) or `"no"`
111+
- **Default:** Trevor Project help page
112+
- **Description:** Custom help page URL or hide the help button entirely.
113+
- **Special Values:**
114+
- `"no"` - Hides the help button
115+
- Any valid URL - Redirects to custom help page
116+
117+
### Home/Logo Link (`homeUrl`)
118+
119+
- **Type:** `string` (URL) or `"no"`
120+
- **Default:** Trevor Project homepage
121+
- **Description:** Custom home page URL when clicking the logo or hide the home link entirely.
122+
- **Special Values:**
123+
- `"no"` - Removes home link functionality from logo
124+
- Any valid URL - Redirects to custom home page
125+
126+
## Complete Examples
127+
128+
### Basic Embed
129+
130+
```html
131+
<iframe src="https://rainbowrelax.web.app/" width="100%" height="600"></iframe>
132+
```
133+
134+
### Custom Branding
135+
136+
```html
137+
<iframe
138+
src="https://rainbowrelax.web.app/?logoUrl=https://example.com/logo.svg&backgroundUrl=https://example.com/ambient.mp3"
139+
width="100%"
140+
height="600">
141+
</iframe>
142+
```
143+
144+
### Hidden Navigation
145+
146+
```html
147+
<iframe
148+
src="https://rainbowrelax.web.app/?donationUrl=no&helpUrl=no&homeUrl=no"
149+
width="100%"
150+
height="600">
151+
</iframe>
152+
```
153+
154+
### All Parameters
155+
156+
```html
157+
<iframe
158+
src="https://rainbowrelax.web.app/?showquickescape=true&logoUrl=https://example.com/logo.svg&backgroundUrl=https://example.com/ambient.mp3&instructionsUrl=https://example.com/instructions.mp3&guidedVoiceUrl=https://example.com/voice.mp3&audioUrl=https://example.com/breathing.mp3&donationUrl=https://example.com/donate&helpUrl=https://example.com/help&homeUrl=https://example.com"
159+
width="100%"
160+
height="600">
161+
</iframe>
162+
```
163+
164+
## Best Practices & Notes
165+
166+
### Security & Performance
167+
168+
- **HTTPS Required:** All custom asset URLs must use HTTPS for security
169+
- **CORS Headers:** Ensure your asset servers include proper CORS headers
170+
- **CDN Recommended:** Use a CDN for better performance and reliability
171+
- **Asset Optimization:** Compress images and audio files for faster loading
172+
173+
### Audio Guidelines
174+
175+
- **Format Priority:** MP3 > WAV > OGG (for best browser compatibility)
176+
- **Quality:** 128kbps is sufficient for most use cases
177+
- **Duration:** Keep audio files under 5MB for optimal loading
178+
- **Testing:** Test audio playback across different browsers and devices
179+
180+
### Image Guidelines
181+
182+
- **Format Priority:** SVG > PNG > WebP > JPG
183+
- **Aspect Ratio:** Square (1:1) works best with the existing layout
184+
- **Size Range:** 150x150px to 300x300px recommended
185+
- **Fallback:** The app will gracefully fall back to the default Trevor Project logo if your custom logo fails to load
186+
187+
### URL Encoding
188+
189+
When using special characters in URLs, ensure proper URL encoding:
190+
191+
```javascript
192+
// Example: URL with spaces and special characters
193+
const logoUrl = encodeURIComponent('https://example.com/my logo (v2).svg');
194+
const iframeSrc = `https://rainbowrelax.web.app/?logoUrl=${logoUrl}`;
195+
```
196+
42197
# Deployment
43198

44199
This project uses Firebase Hosting with three environments:
@@ -108,7 +263,7 @@ This project uses GitHub Actions for automated testing, linting, and deployment.
108263
### 2. Playwright Tests (`playwright-tests.yml`)
109264
- **Triggers**: Pull requests and pushes to `main`
110265
- **Purpose**: Run end-to-end tests to ensure functionality
111-
- **Runs**:
266+
- **Runs:**
112267
- ESLint validation
113268
- TypeScript compilation
114269
- Build application
@@ -157,11 +312,62 @@ Before pushing, run locally to catch issues early:
157312

158313
```bash
159314
npm run lint # Check for linting errors
160-
npx tsc --noEmit # Verify TypeScript compilation
315+
npm run typecheck # Verify TypeScript compilation
161316
npm run build # Verify build works
162317
npm run test:e2e # Run all tests
318+
npm run precommit # Run all checks (typecheck + lint + e2e tests)
163319
```
164320

321+
---
322+
## Widget Parameter Persistence
323+
324+
The application automatically preserves widget configuration parameters across React Router navigation. This ensures that custom branding and configuration (logoUrl, audioUrl, etc.) persist when users navigate between pages.
325+
326+
### How It Works
327+
328+
Widget configuration parameters are automatically extracted from URL query parameters and preserved during navigation:
329+
330+
- **Internal Navigation**: All internal links use `useNavigateWithParams()` hook or `NavLinkWithParams` component
331+
- **Parameter Preservation**: These components automatically append widget config parameters to navigation URLs
332+
- **Supported Parameters**: `logoUrl`, `backgroundUrl`, `instructionsUrl`, `guidedVoiceUrl`, `audioUrl`, `donationUrl`, `helpUrl`, `homeUrl`
333+
334+
### Developer Usage
335+
336+
**For Programmatic Navigation:**
337+
```typescript
338+
import { useNavigateWithParams } from '../hooks/useNavigateWithParams';
339+
340+
function MyComponent() {
341+
const navigate = useNavigateWithParams();
342+
343+
// Navigate with automatic parameter preservation
344+
navigate('/breathing');
345+
navigate('/thank-you', { state: { minutes: 3 } });
346+
}
347+
```
348+
349+
**For Declarative Navigation:**
350+
```typescript
351+
import { NavLinkWithParams } from '../components/common/NavLinkWithParams';
352+
353+
function MyComponent() {
354+
return (
355+
<NavLinkWithParams to="/breathing">
356+
Start Exercise
357+
</NavLinkWithParams>
358+
);
359+
}
360+
```
361+
362+
**External Links:**
363+
External links (donation, help, home) use standard `<a>` tags with `target="_blank"` and `rel="noopener"` for security.
364+
365+
### Implementation Details
366+
367+
- `navigationHelpers.ts`: Utility functions for extracting and building URLs with widget parameters
368+
- `useNavigateWithParams.ts`: Custom hook wrapping React Router's `useNavigate` with parameter preservation
369+
- `NavLinkWithParams.tsx`: Enhanced NavLink component with automatic parameter preservation
370+
165371
---
166372
# Audio Credits
167373

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"test:e2e": "playwright test --config=tests/playwright.config.ts",
1212
"test:e2e:ui": "playwright test --config=tests/playwright.config.ts --ui",
1313
"test:e2e:headed": "playwright test --config=tests/playwright.config.ts --headed",
14-
"test:e2e:report": "playwright show-report"
14+
"test:e2e:report": "playwright show-report",
15+
"precommit": "npm run typecheck && npm run lint && npm run build && npm run test:e2e",
16+
"typecheck": "tsc -b --noEmit"
1517
},
1618
"dependencies": {
1719
"@tailwindcss/vite": "^4.0.1",
@@ -48,4 +50,4 @@
4850
"typescript-eslint": "^8.18.2",
4951
"vite": "^6.3.5"
5052
}
51-
}
53+
}

src/components/GA4.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,33 @@ import { useTranslation } from "react-i18next";
55

66
export default function GA4() {
77
const { t } = useTranslation();
8-
const MEASUREMENT_ID = import.meta.env.VITE_GTAG;
9-
const inited = useRef(false);
8+
const gtag = import.meta.env.VITE_GTAG;
9+
if (gtag==null)
10+
{
11+
return null
12+
}else
13+
{
14+
return (
15+
<CookieConsent
16+
location="bottom"
17+
buttonText={t("acceptcookie")}
18+
cookieName="cookie1"
19+
style={{ background: "#ff5a3e" }}
20+
buttonStyle={{ color: "#595c3fff", fontSize: "13px" }}
21+
expires={150}
22+
onAccept={(acceptedByScrolling) => {
23+
if (!acceptedByScrolling) {
24+
ReactGA.initialize(gtag, {
25+
gaOptions: {
26+
anonymizeIp: true,
27+
},
28+
})
29+
}
30+
}}
31+
enableDeclineButton flipButtons
32+
declineButtonText={t("declinecookie")}
33+
onDecline={() => {
34+
}}
1035

1136
useEffect(() => {
1237
ReactGA.gtag("consent", "default", {

src/components/Instructions.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ export default function BreathingInstructions({
6868

6969
useEffect(() => {
7070
initAudio(exerciseType);
71-
}, [initAudio, exerciseType]);
71+
}, [exerciseType]);
7272

7373
useEffect(() => {
7474
setBackgroundMusic(isSoundEnabled && shouldPlayMusic);
7575
setGuidedVoice(isSoundEnabled && showIntro);
76-
}, [isSoundEnabled, shouldPlayMusic, setBackgroundMusic, setGuidedVoice, showIntro]);
76+
}, [isSoundEnabled, shouldPlayMusic, showIntro]);
7777

7878
useEffect(() => {
7979
if (timeLeft === 0 && !showIntro && !exerciseCompleted) {
@@ -118,7 +118,7 @@ export default function BreathingInstructions({
118118
);
119119
hasResetRef.current = false;
120120
};
121-
}, [resetAnimation, resetExercise]);
121+
}, []);
122122

123123
useEffect(() => {
124124
if (!animationSet.waitSet) {
@@ -136,20 +136,20 @@ export default function BreathingInstructions({
136136
window.clearTimeout(animationTimeoutRef.current);
137137
}
138138
};
139-
}, [changeAnimation, animationSet.waitSet]);
139+
}, [animationSet.waitSet]);
140140

141141
useEffect(() => {
142142
return () => {
143143
stopMusicAndInstructions();
144144
};
145-
}, [stopMusicAndInstructions]);
145+
}, []);
146146

147147
useEffect(() => {
148148
if (!showIntro && !animationSet.exerciseSet) {
149149
changeAnimation("4-7-8");
150150
setAnimationSet((prev) => ({ ...prev, exerciseSet: true }));
151151
}
152-
}, [showIntro, changeAnimation, animationSet.exerciseSet]);
152+
}, [showIntro, animationSet.exerciseSet]);
153153

154154
const handlePauseToggle = () => {
155155
togglePause();

src/components/NavBar.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ const helpUrl = config.helpUrl;
2626

2727

2828
<div className="flex items-center space-x-4">
29-
{/* Language toggle button*/}
3029
<ToggleButton />
3130

32-
{/* Donate button - show if URL is provided */}
3331
{donateUrl && (
3432
<a
3533
href={donateUrl} target="_blank" rel="noopener"
@@ -39,7 +37,6 @@ const helpUrl = config.helpUrl;
3937
</a>
4038
)}
4139

42-
{/* Help button - show if URL is provided */}
4340
{helpUrl && (
4441
<a
4542
href={helpUrl} target="_blank" rel="noopener"

src/components/QuickStartPreset.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useTranslation } from "react-i18next";
22
import { useState } from "react";
33
import { Timer } from "lucide-react";
4-
import { useNavigate } from "react-router-dom";
4+
import { useNavigateWithParams } from "../hooks/useNavigateWithParams";
55

66
interface Params {
77
onClick: (cycles: number) => void;
@@ -11,7 +11,7 @@ const QuickStartPreset = ({ onClick }: Params) => {
1111
const [showCustomOptions, setShowCustomOptions] = useState(false);
1212
const [customMinutes, setCustomMinutes] = useState<number | string>("");
1313
const isCustomValid = customMinutes !== "" && Number(customMinutes) >= 1;
14-
const navigate = useNavigate();
14+
const navigate = useNavigateWithParams();
1515

1616
const handleNavigate = (minutes: number) => {
1717
onClick(minutes);

0 commit comments

Comments
 (0)