feat: add breathing exercises tool

This commit is contained in:
MAZE 2024-08-30 15:12:11 +03:30
parent f526f97908
commit 27f25785e1
6 changed files with 279 additions and 0 deletions

View 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>

View 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;
}

View 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>
);
}

View file

@ -0,0 +1 @@
export { Breathing } from './breathing';

View 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>

View 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>