mirror of
https://github.com/remvze/moodist.git
synced 2025-12-17 08:54:13 +00:00
feat: add breathing exercises tool
This commit is contained in:
parent
f526f97908
commit
27f25785e1
6 changed files with 279 additions and 0 deletions
33
src/components/tools/about.astro
Normal file
33
src/components/tools/about.astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { Container } from '../container';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="about">
|
||||
<Container tight>
|
||||
<h2 class="title">About this tool</h2>
|
||||
<p class="text">{text}</p>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.about {
|
||||
margin-top: 75px;
|
||||
|
||||
& .title {
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& .text {
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
src/components/tools/breathing/breathing.module.css
Normal file
47
src/components/tools/breathing/breathing.module.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
.exercise {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 75px 0;
|
||||
margin-top: 12px;
|
||||
background-color: var(--color-neutral-50);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
|
||||
& .phase {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& .circle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: -1;
|
||||
height: 55%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-image: radial-gradient(
|
||||
var(--color-neutral-50),
|
||||
var(--color-neutral-100)
|
||||
);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.selectBox {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 45px;
|
||||
padding: 0 12px;
|
||||
margin-top: 8px;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
}
|
||||
124
src/components/tools/breathing/breathing.tsx
Normal file
124
src/components/tools/breathing/breathing.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { Container } from '@/components/container';
|
||||
|
||||
import styles from './breathing.module.css';
|
||||
|
||||
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
|
||||
type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
|
||||
|
||||
export function Breathing() {
|
||||
const [selectedExercise, setSelectedExercise] =
|
||||
useState<Exercise>('4-7-8 Breathing');
|
||||
|
||||
const getAnimationPhases = (
|
||||
exercise: Exercise,
|
||||
): Array<'inhale' | 'holdInhale' | 'exhale' | 'holdExhale'> => {
|
||||
switch (exercise) {
|
||||
case 'Box Breathing':
|
||||
return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
|
||||
case 'Resonant Breathing':
|
||||
return ['inhale', 'exhale'];
|
||||
case '4-7-8 Breathing':
|
||||
return ['inhale', 'holdInhale', 'exhale'];
|
||||
default:
|
||||
return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
|
||||
}
|
||||
};
|
||||
|
||||
const getAnimationDurations = (exercise: Exercise) => {
|
||||
switch (exercise) {
|
||||
case 'Box Breathing':
|
||||
return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
|
||||
case 'Resonant Breathing':
|
||||
return { exhale: 5, inhale: 5 };
|
||||
case '4-7-8 Breathing':
|
||||
return { exhale: 8, holdInhale: 7, inhale: 4 };
|
||||
default:
|
||||
return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = (phase: Phase) => {
|
||||
switch (phase) {
|
||||
case 'inhale':
|
||||
return 'Inhale';
|
||||
case 'exhale':
|
||||
return 'Exhale';
|
||||
default:
|
||||
return 'Hold';
|
||||
}
|
||||
};
|
||||
|
||||
const [phase, setPhase] = useState<Phase>('inhale');
|
||||
const [durations, setDurations] = useState(
|
||||
getAnimationDurations(selectedExercise),
|
||||
);
|
||||
|
||||
const animationVariants = {
|
||||
exhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
transition: { duration: durations.exhale },
|
||||
},
|
||||
holdExhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
transition: { duration: durations.holdExhale || 4 },
|
||||
},
|
||||
holdInhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1.5)',
|
||||
transition: { duration: durations.holdInhale || 4 },
|
||||
},
|
||||
inhale: {
|
||||
transform: 'translate(-50%, -50%) scale(1.5)',
|
||||
transition: { duration: durations.inhale },
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDurations(getAnimationDurations(selectedExercise));
|
||||
}, [selectedExercise]);
|
||||
|
||||
useEffect(() => {
|
||||
const phases = getAnimationPhases(selectedExercise);
|
||||
|
||||
let phaseIndex = 0;
|
||||
|
||||
setPhase(phases[phaseIndex]);
|
||||
|
||||
const interval = setInterval(
|
||||
() => {
|
||||
phaseIndex = (phaseIndex + 1) % phases.length;
|
||||
|
||||
setPhase(phases[phaseIndex]);
|
||||
},
|
||||
(durations[phases[phaseIndex]] || 4) * 1000,
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedExercise, durations]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className={styles.exercise}>
|
||||
<motion.div
|
||||
animate={phase}
|
||||
className={styles.circle}
|
||||
key={selectedExercise}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<p className={styles.phase}>{getLabel(phase)}</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className={styles.selectBox}
|
||||
value={selectedExercise}
|
||||
onChange={e => setSelectedExercise(e.target.value as Exercise)}
|
||||
>
|
||||
<option value="Box Breathing">Box Breathing</option>
|
||||
<option value="Resonant Breathing">Resonant Breathing</option>
|
||||
<option value="4-7-8 Breathing">4-7-8 Breathing</option>
|
||||
</select>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
1
src/components/tools/breathing/index.ts
Normal file
1
src/components/tools/breathing/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Breathing } from './breathing';
|
||||
55
src/components/tools/hero.astro
Normal file
55
src/components/tools/hero.astro
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
import { Container } from '../container';
|
||||
|
||||
interface Props {
|
||||
desc: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { desc, title } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<Container>
|
||||
<img alt="Moodist Logo" class="logo" src="/logo.svg" />
|
||||
<h1>{title}</h1>
|
||||
<p class="desc">{desc}</p>
|
||||
<p class="moodist">Part of <a href="/">Moodist</a></p>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: 75px 0;
|
||||
text-align: center;
|
||||
|
||||
& .logo {
|
||||
display: block;
|
||||
width: 45px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 4px;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.moodist {
|
||||
margin-top: 16px;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-foreground-subtle);
|
||||
|
||||
& a {
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/pages/tools/breathing-exercises.astro
Normal file
19
src/pages/tools/breathing-exercises.astro
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/tools/hero.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import About from '@/components/tools/about.astro';
|
||||
import { Breathing } from '@/components/tools/breathing';
|
||||
---
|
||||
|
||||
<Layout title="Breathing Exercises Tool — Moodist">
|
||||
<Donate />
|
||||
<Hero desc="Simple breathing exercises." title="Breathing Exercises" />
|
||||
<Breathing client:load />
|
||||
<About
|
||||
text="Experience calm and focus with our simple breathing exercise tool. Designed to guide your breath, it helps reduce stress, enhance relaxation, and improve mindfulness in just a few minutes a day."
|
||||
/>
|
||||
<Footer />
|
||||
</Layout>
|
||||
Loading…
Add table
Reference in a new issue