├── .npmrc
├── .eslintignore
├── .prettierignore
├── .stylelintignore
├── src
├── components
│ ├── app
│ │ ├── index.ts
│ │ ├── app.module.css
│ │ └── app.tsx
│ ├── container
│ │ ├── index.ts
│ │ ├── container.module.css
│ │ └── container.tsx
│ ├── footer.astro
│ ├── hero.astro
│ └── note.astro
├── styles
│ ├── variables
│ │ ├── index.css
│ │ ├── color.css
│ │ └── typography.css
│ ├── global.css
│ ├── base
│ │ └── base.css
│ └── fonts.css
├── env.d.ts
├── helpers
│ ├── number.ts
│ ├── string.ts
│ ├── styles.ts
│ └── random.ts
├── pages
│ └── index.astro
├── hooks
│ ├── use-sound.ts
│ └── use-local-storage.ts
└── layouts
│ └── layout.astro
├── .czrc
├── public
├── og.png
├── chime.mp3
├── images
│ └── noise.png
├── robots.txt
├── fonts
│ ├── inter-v13-latin-500.woff2
│ ├── lora-v35-latin-regular.woff2
│ ├── inter-v13-latin-regular.woff2
│ ├── space-mono-v13-latin-700.woff2
│ ├── space-mono-v13-latin-regular.woff2
│ └── instrument-serif-v4-latin-regular.woff2
├── logo.svg
└── favicon.svg
├── .commitlintrc.json
├── .editorconfig
├── .husky
├── pre-commit
└── commit-msg
├── postcss.config.cjs
├── Caddyfile
├── .prettierrc.json
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── docker-compose.yml
├── README.md
├── .lintstagedrc.json
├── tsconfig.json
├── Dockerfile
├── astro.config.mjs
├── .gitignore
├── .stylelintrc.json
├── .versionrc.json
├── .github
└── workflows
│ └── build_docker.yml
├── LICENSE
├── package.json
├── .eslintrc.json
└── CHANGELOG.md
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .output/
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .output/
4 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .output/
4 |
--------------------------------------------------------------------------------
/src/components/app/index.ts:
--------------------------------------------------------------------------------
1 | export { App } from './app';
2 |
--------------------------------------------------------------------------------
/.czrc:
--------------------------------------------------------------------------------
1 | {
2 | "path": "./node_modules/cz-conventional-changelog"
3 | }
4 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/og.png
--------------------------------------------------------------------------------
/src/components/container/index.ts:
--------------------------------------------------------------------------------
1 | export { Container } from './container';
2 |
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/chime.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/chime.mp3
--------------------------------------------------------------------------------
/src/styles/variables/index.css:
--------------------------------------------------------------------------------
1 | @import 'color.css';
2 | @import 'typography.css';
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/public/images/noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/images/noise.png
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
4 | Sitemap: https://calmness.mvze.net/sitemap-index.xml
5 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @import 'variables/index.css';
2 | @import 'base/base.css';
3 | @import 'fonts.css';
4 |
--------------------------------------------------------------------------------
/public/fonts/inter-v13-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/fonts/inter-v13-latin-500.woff2
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | 'postcss-nesting': {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/fonts/lora-v35-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/fonts/lora-v35-latin-regular.woff2
--------------------------------------------------------------------------------
/public/fonts/inter-v13-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/fonts/inter-v13-latin-regular.woff2
--------------------------------------------------------------------------------
/public/fonts/space-mono-v13-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/fonts/space-mono-v13-latin-700.woff2
--------------------------------------------------------------------------------
/public/fonts/space-mono-v13-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/fonts/space-mono-v13-latin-regular.woff2
--------------------------------------------------------------------------------
/public/fonts/instrument-serif-v4-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remvze/calmness/HEAD/public/fonts/instrument-serif-v4-latin-regular.woff2
--------------------------------------------------------------------------------
/src/helpers/number.ts:
--------------------------------------------------------------------------------
1 | export function padNumber(number: number, maxLength: number = 2): string {
2 | return number.toString().padStart(maxLength, '0');
3 | }
4 |
--------------------------------------------------------------------------------
/Caddyfile:
--------------------------------------------------------------------------------
1 | :8080 {
2 | file_server
3 | root * /var/www/html
4 |
5 | handle_errors {
6 | rewrite * /index.html
7 | file_server
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '@/layouts/layout.astro';
3 | import { App } from '@/components/app';
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-astro"],
3 | "singleQuote": true,
4 | "arrowParens": "avoid",
5 | "endOfLine": "lf",
6 | "tabWidth": 2,
7 | "semi": true
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/container/container.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 90%;
3 | max-width: 450px;
4 | margin: 0 auto;
5 |
6 | &.wide {
7 | max-width: 850px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/string.ts:
--------------------------------------------------------------------------------
1 | export function truncateString(str: string, num: number) {
2 | if (str.length > num) {
3 | return str.slice(0, num) + '...';
4 | } else {
5 | return str;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "astro-build.astro-vscode",
4 | "esbenp.prettier-vscode",
5 | "dbaeumer.vscode-eslint",
6 | "stylelint.vscode-stylelint"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | timesy:
3 | image: ghcr.io/remvze/calmness
4 | logging:
5 | options:
6 | max-size: 1g
7 | restart: always
8 | ports:
9 | - '8080:8080'
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.{ts,tsx,js,jsx}": "eslint --fix",
3 | "*.{json,md}": "prettier --write",
4 | "*.css": "stylelint --fix",
5 | "*.astro": ["eslint --fix", "stylelint --fix"],
6 | "*.html": ["prettier --write", "stylelint --fix"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/helpers/styles.ts:
--------------------------------------------------------------------------------
1 | type className = undefined | null | false | string;
2 |
3 | export function cn(...classNames: Array): string {
4 | const className = classNames.filter(className => !!className).join(' ');
5 |
6 | return className;
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "react",
6 | "baseUrl": "./src",
7 | "paths": {
8 | "@/*": ["./*"],
9 | },
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM docker.io/node:20-alpine3.18 AS build
2 | WORKDIR /app
3 | COPY package*.json ./
4 | RUN npm install
5 | COPY . .
6 | RUN npm run build
7 |
8 | FROM docker.io/caddy:latest
9 | COPY ./Caddyfile /etc/caddy/Caddyfile
10 | COPY --from=build /app/dist /var/www/html
11 |
12 | EXPOSE 8080
13 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config';
2 | import react from '@astrojs/react';
3 |
4 | import sitemap from '@astrojs/sitemap';
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | integrations: [react(), sitemap()],
9 | site: 'https://calmness.mvze.net',
10 | });
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/src/components/container/container.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/helpers/styles';
2 |
3 | import styles from './container.module.css';
4 |
5 | interface ContainerProps {
6 | children: React.ReactNode;
7 | wide?: boolean;
8 | }
9 |
10 | export function Container({ children, wide }: ContainerProps) {
11 | return (
12 | {children}
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/base/base.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | padding: 0;
6 | margin: 0;
7 | font: inherit;
8 | }
9 |
10 | html {
11 | scroll-behavior: smooth;
12 | }
13 |
14 | body {
15 | font-family: var(--font-sans);
16 | font-size: var(--font-base);
17 | color: var(--color-foreground);
18 | background-color: var(--color-neutral-50);
19 | }
20 |
21 | ::selection {
22 | color: var(--color-foreground);
23 | background-color: var(--color-neutral-300);
24 | }
25 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-standard",
4 | "stylelint-config-recess-order",
5 | "stylelint-config-html",
6 | "stylelint-prettier/recommended"
7 | ],
8 |
9 | "rules": {
10 | "import-notation": "string",
11 | "selector-class-pattern": null,
12 | "no-descending-specificity": null
13 | },
14 |
15 | "overrides": [
16 | {
17 | "files": ["*.astro"],
18 | "rules": {
19 | "prettier/prettier": null
20 | }
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/use-sound.ts:
--------------------------------------------------------------------------------
1 | import { useMemo, useCallback, useEffect } from 'react';
2 |
3 | export function useSound(url: string, volume: number = 0.5) {
4 | const audio = useMemo(() => {
5 | if (typeof window !== 'undefined') return new Audio(url);
6 |
7 | return null;
8 | }, [url]);
9 |
10 | useEffect(() => {
11 | if (audio) {
12 | audio.volume = volume;
13 | }
14 | }, [volume, audio]);
15 |
16 | const play = useCallback(() => {
17 | if (audio) audio.play();
18 | }, [audio]);
19 |
20 | return play;
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.eol": "\n",
3 | "eslint.validate": [
4 | "javascript",
5 | "javascriptreact",
6 | "typescript",
7 | "typescriptreact",
8 | "astro"
9 | ],
10 | "stylelint.validate": ["css", "html", "astro"],
11 | "editor.defaultFormatter": "esbenp.prettier-vscode",
12 | "editor.formatOnSave": true,
13 | "editor.codeActionsOnSave": {
14 | "source.fixAll.eslint": "explicit",
15 | "source.fixAll.stylelint": "explicit"
16 | },
17 | "[javascript][javascriptreact][typescript][typescriptreact][astro]": {
18 | "editor.formatOnSave": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/styles/variables/color.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-neutral-50: #09090b;
3 | --color-neutral-100: #18181b;
4 | --color-neutral-200: #27272a;
5 | --color-neutral-300: #3f3f46;
6 | --color-neutral-400: #52525b;
7 | --color-neutral-500: #71717a;
8 | --color-neutral-600: #a1a1aa;
9 | --color-neutral-700: #d4d4d8;
10 | --color-neutral-800: #e4e4e7;
11 | --color-neutral-900: #f4f4f5;
12 | --color-neutral-950: #fafafa;
13 |
14 | /* Foreground */
15 | --color-foreground: var(--color-neutral-950);
16 | --color-foreground-subtle: var(--color-neutral-600);
17 | --color-foreground-subtler: var(--color-neutral-500);
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/random.ts:
--------------------------------------------------------------------------------
1 | export function random(min: number, max: number): number {
2 | return Math.random() * (max - min) + min;
3 | }
4 |
5 | export function randomInt(min: number, max: number): number {
6 | return Math.floor(random(min, max));
7 | }
8 |
9 | export function pick(array: Array): T {
10 | const randomIndex = randomInt(0, array.length);
11 |
12 | return array[randomIndex];
13 | }
14 |
15 | export function pickMany(array: Array, count: number): Array {
16 | const shuffled = shuffle(array);
17 |
18 | return shuffled.slice(0, count);
19 | }
20 |
21 | export function shuffle(array: Array): Array {
22 | return array
23 | .map(value => ({ sort: Math.random(), value }))
24 | .sort((a, b) => a.sort - b.sort)
25 | .map(({ value }) => value);
26 | }
27 |
--------------------------------------------------------------------------------
/.versionrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "types": [
3 | {
4 | "type": "feat",
5 | "section": "✨ Features"
6 | },
7 | {
8 | "type": "fix",
9 | "section": "🐛 Bug Fixes"
10 | },
11 | {
12 | "type": "chore",
13 | "hidden": false,
14 | "section": "🚚 Chores"
15 | },
16 | {
17 | "type": "docs",
18 | "hidden": false,
19 | "section": "📝 Documentation"
20 | },
21 | {
22 | "type": "style",
23 | "hidden": false,
24 | "section": "💄 Styling"
25 | },
26 | {
27 | "type": "refactor",
28 | "hidden": false,
29 | "section": "♻️ Code Refactoring"
30 | },
31 | {
32 | "type": "perf",
33 | "hidden": false,
34 | "section": "⚡️ Performance Improvements"
35 | },
36 | {
37 | "type": "test",
38 | "hidden": false,
39 | "section": "✅ Testing"
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/src/styles/variables/typography.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-serif: 'Lora', serif;
3 | --font-display: 'Instrument Serif', serif;
4 | --font-sans: 'Inter', sans-serif;
5 | --font-mono: 'Space Mono', monospace;
6 |
7 | /* Font Sizes */
8 | --font-base-size: 1rem;
9 | --font-pos-ratio: 1.2;
10 | --font-neg-ratio: 1.125;
11 | --font-3xlg: calc(var(--font-xxlg) * var(--font-pos-ratio));
12 | --font-2xlg: calc(var(--font-xlg) * var(--font-pos-ratio));
13 | --font-xlg: calc(var(--font-lg) * var(--font-pos-ratio));
14 | --font-lg: calc(var(--font-md) * var(--font-pos-ratio));
15 | --font-md: calc(var(--font-base) * var(--font-pos-ratio));
16 | --font-base: var(--font-base-size);
17 | --font-sm: calc(var(--font-base) / var(--font-neg-ratio));
18 | --font-xsm: calc(var(--font-sm) / var(--font-neg-ratio));
19 | --font-2xsm: calc(var(--font-xsm) / var(--font-neg-ratio));
20 | --font-3xsm: calc(var(--font-xxsm) / var(--font-neg-ratio));
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/build_docker.yml:
--------------------------------------------------------------------------------
1 | name: Build and push main image
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | push-store-image:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: 'Checkout GitHub Action'
14 | uses: actions/checkout@main
15 |
16 | - name: 'Login to GitHub Container Registry'
17 | uses: docker/login-action@v1
18 | with:
19 | registry: ghcr.io
20 | username: ${{github.actor}}
21 | password: ${{secrets.ACCESS_TOKEN}}
22 |
23 | - name: Set up QEMU
24 | uses: docker/setup-qemu-action@v1
25 |
26 | - name: Set up Docker Buildx
27 | uses: docker/setup-buildx-action@v1
28 |
29 | - name: 'Build and push Inventory Image'
30 | run: |
31 | IMAGE_NAME="ghcr.io/remvze/calmness"
32 |
33 | GIT_TAG=${{ github.ref }}
34 | GIT_TAG=${GIT_TAG#refs/tags/}
35 |
36 | docker buildx build \
37 | --platform linux/amd64,linux/arm64 \
38 | -t $IMAGE_NAME:latest \
39 | -t $IMAGE_NAME:$GIT_TAG \
40 | --push .
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 MAZE
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
2 |
3 | type SetValue = Dispatch>;
4 |
5 | /**
6 | * A custom React hook to manage state with localStorage persistence.
7 | *
8 | * @template T
9 | * @param {string} key - The key under which the value is stored in localStorage.
10 | * @param {T} fallback - The fallback value to use if there is no value in localStorage.
11 | * @returns {[T, SetValue]} An array containing the stateful value and a function to update it.
12 | */
13 | export function useLocalStorage(key: string, fallback: T): [T, SetValue] {
14 | const [value, setValue] = useState(fallback);
15 |
16 | useEffect(() => {
17 | const value = localStorage.getItem(key);
18 |
19 | if (!value) return;
20 |
21 | let parsed;
22 |
23 | try {
24 | parsed = JSON.parse(value);
25 | } catch (error) {
26 | parsed = fallback;
27 | }
28 |
29 | setValue(parsed);
30 | }, [key, JSON.stringify(fallback)]);
31 |
32 | useEffect(() => {
33 | localStorage.setItem(key, JSON.stringify(value));
34 | }, [value, key]);
35 |
36 | return [value, setValue];
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Container } from './container';
3 | ---
4 |
5 |
6 |
21 |
22 |
23 |
54 |
--------------------------------------------------------------------------------
/src/components/hero.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Container } from './container';
3 | ---
4 |
5 |
6 |
7 |
14 | Calmness
15 | Online breathing exercises tool.
16 |
17 |
18 |
19 |
58 |
--------------------------------------------------------------------------------
/src/styles/fonts.css:
--------------------------------------------------------------------------------
1 | /* inter-regular - latin */
2 | @font-face {
3 | font-family: Inter;
4 | font-style: normal;
5 | font-weight: 400;
6 | src: url('/fonts/inter-v13-latin-regular.woff2') format('woff2');
7 | font-display: swap;
8 | }
9 |
10 | /* inter-500 - latin */
11 | @font-face {
12 | font-family: Inter;
13 | font-style: normal;
14 | font-weight: 500;
15 | src: url('/fonts/inter-v13-latin-500.woff2') format('woff2');
16 | font-display: swap;
17 | }
18 |
19 | /* lora-regular - latin */
20 | @font-face {
21 | font-family: Lora;
22 | font-style: normal;
23 | font-weight: 400;
24 | src: url('/fonts/lora-v35-latin-regular.woff2') format('woff2');
25 | font-display: swap;
26 | }
27 |
28 | /* space-mono-regular - latin */
29 | @font-face {
30 | font-family: 'Space Mono';
31 | font-style: normal;
32 | font-weight: 400;
33 | src: url('/fonts/space-mono-v13-latin-regular.woff2') format('woff2');
34 | font-display: swap;
35 | }
36 |
37 | /* space-mono-700 - latin */
38 | @font-face {
39 | font-family: 'Space Mono';
40 | font-style: normal;
41 | font-weight: 700;
42 | src: url('/fonts/space-mono-v13-latin-700.woff2') format('woff2');
43 | font-display: swap;
44 | }
45 |
46 | /* instrument-serif-regular - latin */
47 | @font-face {
48 | font-family: 'Instrument Serif';
49 | font-style: normal;
50 | font-weight: 400;
51 | src: url('/fonts/instrument-serif-v4-latin-regular.woff2') format('woff2');
52 | font-display: swap;
53 | }
54 |
--------------------------------------------------------------------------------
/src/layouts/layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import '@/styles/global.css';
3 |
4 | interface Props {
5 | description?: string;
6 | og?: string;
7 | title?: string;
8 | }
9 |
10 | const title = Astro.props.title
11 | ? `${Astro.props.title} — Calmness`
12 | : 'Calmness: Online Breathing Exercises Tool';
13 |
14 | const description =
15 | Astro.props.description ||
16 | 'A simple and minimal breathing exercises tool in your browser.';
17 |
18 | const og = Astro.props.og || '/og.png';
19 | ---
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {title}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/note.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Container } from './container';
3 | ---
4 |
5 |
6 |
7 | Finding Calm in the Chaos
8 |
9 | In the relentless hustle of modern life, where every tick of the clock
10 | feels like a push to achieve more and move faster, stress wraps around you
11 | like a tightrope you can't escape. The constant pressure to keep up
12 | and outdo yesterday's self can turn each day into a high-stakes game
13 | of survival.
14 |
15 |
16 | But here's your chance to pause the whirlwind. Take a deliberate step
17 | back, hit the pause button on your endless to-do list, and just breathe.
18 | Let your breath cut through the noise, gently pushing away the weight of
19 | your worries. Inhale calm and exhale the chaos, finding a brief, serene
20 | space where it's okay to simply be.
21 |
22 |
23 |
24 |
25 |
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "calmness",
3 | "type": "module",
4 | "version": "0.1.0",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro preview",
10 | "astro": "astro",
11 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro",
12 | "lint:fix": "npm run lint -- --fix",
13 | "lint:style": "stylelint ./**/*.{css,astro,html}",
14 | "lint:style:fix": "npm run lint:style -- --fix",
15 | "format": "prettier . --write",
16 | "prepare": "husky install",
17 | "commit": "git-cz",
18 | "release": "standard-version --no-verify",
19 | "release:major": "npm run release -- --release-as major",
20 | "release:minor": "npm run release -- --release-as minor",
21 | "release:patch": "npm run release -- --release-as patch"
22 | },
23 | "dependencies": {
24 | "@astrojs/react": "^3.0.9",
25 | "@astrojs/sitemap": "3.1.6",
26 | "@types/react": "^18.2.48",
27 | "@types/react-dom": "^18.2.18",
28 | "astro": "^4.2.1",
29 | "framer-motion": "11.5.4",
30 | "react": "^18.2.0",
31 | "react-dom": "^18.2.0",
32 | "react-icons": "5.0.1"
33 | },
34 | "devDependencies": {
35 | "@commitlint/cli": "18.4.4",
36 | "@commitlint/config-conventional": "18.4.4",
37 | "@typescript-eslint/eslint-plugin": "6.19.0",
38 | "@typescript-eslint/parser": "6.19.0",
39 | "astro-eslint-parser": "0.16.2",
40 | "autoprefixer": "10.4.17",
41 | "clipboardy": "4.0.0",
42 | "commitizen": "4.3.0",
43 | "cz-conventional-changelog": "3.3.0",
44 | "eslint": "8.56.0",
45 | "eslint-config-prettier": "9.1.0",
46 | "eslint-import-resolver-alias": "1.1.2",
47 | "eslint-import-resolver-typescript": "3.6.1",
48 | "eslint-plugin-astro": "0.31.3",
49 | "eslint-plugin-import": "2.29.1",
50 | "eslint-plugin-jsx-a11y": "6.8.0",
51 | "eslint-plugin-prettier": "5.1.3",
52 | "eslint-plugin-react": "7.33.2",
53 | "eslint-plugin-react-hooks": "4.6.0",
54 | "eslint-plugin-sort-destructure-keys": "1.5.0",
55 | "eslint-plugin-sort-keys-fix": "1.1.2",
56 | "eslint-plugin-typescript-sort-keys": "3.1.0",
57 | "husky": "8.0.3",
58 | "lint-staged": "15.2.0",
59 | "postcss-html": "1.6.0",
60 | "postcss-nesting": "12.0.2",
61 | "prettier": "3.2.4",
62 | "prettier-plugin-astro": "0.13.0",
63 | "standard-version": "9.5.0",
64 | "stylelint": "16.2.0",
65 | "stylelint-config-html": "1.1.0",
66 | "stylelint-config-recess-order": "4.4.0",
67 | "stylelint-config-standard": "36.0.0",
68 | "stylelint-prettier": "5.0.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 |
4 | "env": {
5 | "browser": true,
6 | "amd": true,
7 | "node": true,
8 | "es2022": true
9 | },
10 |
11 | "parser": "@typescript-eslint/parser",
12 |
13 | "parserOptions": {
14 | "ecmaVersion": "latest",
15 | "sourceType": "module",
16 | "ecmaFeatures": {
17 | "jsx": true
18 | }
19 | },
20 |
21 | "extends": [
22 | "eslint:recommended",
23 | "plugin:@typescript-eslint/recommended",
24 | "plugin:typescript-sort-keys/recommended",
25 | "plugin:import/recommended",
26 | "plugin:react/recommended",
27 | "plugin:react/jsx-runtime",
28 | "plugin:jsx-a11y/recommended",
29 | "plugin:react-hooks/recommended",
30 | "plugin:astro/recommended",
31 | "prettier"
32 | ],
33 |
34 | "plugins": [
35 | "@typescript-eslint",
36 | "typescript-sort-keys",
37 | "sort-keys-fix",
38 | "sort-destructure-keys",
39 | "prettier"
40 | ],
41 |
42 | "rules": {
43 | "prettier/prettier": "error",
44 | "sort-keys-fix/sort-keys-fix": ["warn", "asc"],
45 | "sort-destructure-keys/sort-destructure-keys": "warn",
46 | "@typescript-eslint/triple-slash-reference": "off",
47 | "jsx-a11y/no-static-element-interactions": "off",
48 | "react/jsx-sort-props": [
49 | "warn",
50 | {
51 | "callbacksLast": true,
52 | "multiline": "last"
53 | }
54 | ]
55 | },
56 |
57 | "settings": {
58 | "react": {
59 | "version": "detect"
60 | },
61 |
62 | "import/parsers": {
63 | "@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx"]
64 | },
65 |
66 | "import/resolver": {
67 | "typescript": true,
68 | "node": true,
69 | "alias": {
70 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
71 | "map": [["@", "./src"]]
72 | }
73 | }
74 | },
75 |
76 | "overrides": [
77 | {
78 | "files": ["**/*.astro"],
79 | "parser": "astro-eslint-parser",
80 |
81 | "parserOptions": {
82 | "parser": "@typescript-eslint/parser",
83 | "extraFileExtensions": [".astro"]
84 | },
85 |
86 | "rules": {
87 | "prettier/prettier": "error",
88 | "react/no-unknown-property": "off",
89 | "react/jsx-key": "off"
90 | },
91 |
92 | "globals": {
93 | "Astro": "readonly"
94 | }
95 | },
96 |
97 | {
98 | "files": ["**/*.astro/*.js"],
99 | "rules": {
100 | "prettier/prettier": "off"
101 | }
102 | }
103 | ]
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/app/app.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | height: auto;
6 | min-height: 100dvh;
7 | padding: 200px 0;
8 | }
9 |
10 | .srOnly {
11 | position: absolute;
12 | width: 1px;
13 | height: 1px;
14 | padding: 0;
15 | margin: -1px;
16 | overflow: hidden;
17 | clip: rect(0, 0, 0, 0);
18 | white-space: nowrap;
19 | border: 0;
20 | }
21 |
22 | .info {
23 | position: absolute;
24 | bottom: 20px;
25 | left: 0;
26 | width: 100%;
27 |
28 | & .buttons {
29 | display: flex;
30 | column-gap: 4px;
31 | align-items: center;
32 | justify-content: center;
33 | margin-bottom: 12px;
34 |
35 | & button {
36 | height: 32px;
37 | padding: 0 20px;
38 | font-size: var(--font-2xsm);
39 | font-weight: 500;
40 | color: var(--color-foreground);
41 | cursor: pointer;
42 | background: linear-gradient(
43 | var(--color-neutral-50),
44 | var(--color-neutral-100)
45 | );
46 | border: 1px solid var(--color-neutral-300);
47 | border-radius: 999px;
48 | box-shadow: inset 0 -1px 0 var(--color-neutral-500);
49 | transition: 0.2s;
50 |
51 | &:hover {
52 | transform: translateY(-2px);
53 | }
54 | }
55 | }
56 |
57 | & .tip {
58 | font-size: var(--font-sm);
59 | color: var(--color-foreground-subtler);
60 | text-align: center;
61 | }
62 | }
63 |
64 | .circle {
65 | position: relative;
66 |
67 | & .controls {
68 | position: absolute;
69 | inset: 0;
70 | display: flex;
71 | align-items: center;
72 | justify-content: center;
73 |
74 | & div {
75 | background: radial-gradient(
76 | var(--color-neutral-50),
77 | var(--color-neutral-50),
78 | var(--color-neutral-200)
79 | );
80 | border: 1px solid var(--color-neutral-300);
81 | border-radius: 50%;
82 | box-shadow: 0 0 20px rgb(0 0 0 / 10%);
83 | }
84 | }
85 |
86 | & .state {
87 | position: absolute;
88 | inset: 0;
89 | display: flex;
90 | flex-direction: column;
91 | align-items: center;
92 | justify-content: center;
93 |
94 | & .phase {
95 | font-family: var(--font-display);
96 | font-size: var(--font-xlg);
97 | }
98 | }
99 |
100 | & .time {
101 | display: flex;
102 | column-gap: 8px;
103 | align-items: center;
104 | justify-content: center;
105 | margin-top: 20px;
106 | font-family: var(--font-mono);
107 | font-size: var(--font-2xsm);
108 | font-weight: 700;
109 | font-variant-numeric: tabular-nums;
110 | color: var(--color-foreground-subtle);
111 | text-align: center;
112 | text-transform: uppercase;
113 |
114 | & span {
115 | display: block;
116 | width: 50px;
117 | height: 1px;
118 | background: var(--color-neutral-200);
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [0.1.0](https://github.com/remvze/calmness/compare/v0.0.5...v0.1.0) (2025-12-04)
6 |
7 |
8 | ### 🚚 Chores
9 |
10 | * update the domain ([faae93f](https://github.com/remvze/calmness/commit/faae93f0919a1346696125f7abdeaac8868f131c))
11 |
12 |
13 | ### ✨ Features
14 |
15 | * remake ([f091a9d](https://github.com/remvze/calmness/commit/f091a9d35b42881769290aba6c6ceb2a7d093763))
16 |
17 | ### [0.0.5](https://github.com/remvze/calmness/compare/v0.0.4...v0.0.5) (2024-09-19)
18 |
19 |
20 | ### 🐛 Bug Fixes
21 |
22 | * change muted state ([21daeaf](https://github.com/remvze/calmness/commit/21daeaf4d51bd8f510b5c842f95a8d9749e61059))
23 |
24 | ### [0.0.4](https://github.com/remvze/calmness/compare/v0.0.3...v0.0.4) (2024-09-19)
25 |
26 |
27 | ### 🐛 Bug Fixes
28 |
29 | * change progress logic ([052e119](https://github.com/remvze/calmness/commit/052e11918b711f6d86aad1ec9b6334ff97dae2f2))
30 |
31 | ### [0.0.3](https://github.com/remvze/calmness/compare/v0.0.2...v0.0.3) (2024-09-19)
32 |
33 |
34 | ### ✨ Features
35 |
36 | * add progress bar ([e79e1e2](https://github.com/remvze/calmness/commit/e79e1e22344cb40d7d3f90fa2586bfd5ab7b3252))
37 |
38 | ### [0.0.2](https://github.com/remvze/calmness/compare/v0.0.1...v0.0.2) (2024-09-19)
39 |
40 |
41 | ### 💄 Styling
42 |
43 | * change exercise to exercises ([6c48376](https://github.com/remvze/calmness/commit/6c483762cc5f72116f7d4aabd40bb042de23b59d))
44 |
45 | ### 0.0.1 (2024-09-19)
46 |
47 |
48 | ### 💄 Styling
49 |
50 | * minor changes ([a24d443](https://github.com/remvze/calmness/commit/a24d4439aa5da3ed2600ca17471fe8f56496f713))
51 |
52 |
53 | ### 🐛 Bug Fixes
54 |
55 | * decrease font weight ([3fc4409](https://github.com/remvze/calmness/commit/3fc44098983665c0b8d38d5e40d89486ddbcc6e7))
56 |
57 |
58 | ### ✨ Features
59 |
60 | * add custom exercise ([08baf8f](https://github.com/remvze/calmness/commit/08baf8fd1bff747a163251225f965e80e6fa0910))
61 | * add description for exercises ([4a51210](https://github.com/remvze/calmness/commit/4a512107cdd5783c88e073d942fa43fce8c77e75))
62 | * add footer ([b32d318](https://github.com/remvze/calmness/commit/b32d318a2960b6e29b694bb5eaab69b2e22b5216))
63 | * add hero ([b1b1a12](https://github.com/remvze/calmness/commit/b1b1a125c9a8a03a3b71fce9377dfba61c1362dc))
64 | * add logo ([868be19](https://github.com/remvze/calmness/commit/868be198913e9ca2d353f90db816e72687038135))
65 | * add more exercises ([b1d081f](https://github.com/remvze/calmness/commit/b1d081fb95d830d88ebe7873ef4adab6ca7fa7ca))
66 | * add note section ([4cd79cf](https://github.com/remvze/calmness/commit/4cd79cfef713a74fb57698ce5003709468ddfef7))
67 | * add sound ([b9b4f5f](https://github.com/remvze/calmness/commit/b9b4f5f649f57873896061ee4b722c00306cadec))
68 | * implement the main functionality ([b4fdd4a](https://github.com/remvze/calmness/commit/b4fdd4a5672ef6f62d52d8c6219197dff1a2da27))
69 | * initial commit ([c603833](https://github.com/remvze/calmness/commit/c60383332aa0cc40b9392b90da5ec4ce01a933ab))
70 | * make the custom settings persistent ([44ec900](https://github.com/remvze/calmness/commit/44ec90023f1ea1163b69e1474e10909b49ec428f))
71 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/app/app.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | import styles from './app.module.css';
5 | import { cn } from '@/helpers/styles';
6 | import { Container } from '../container';
7 |
8 | export type Phase = 'inhale' | 'hold1' | 'exhale' | 'hold2';
9 |
10 | export interface InfiniteBoxBreathingProps {
11 | autoStart?: boolean;
12 | className?: string;
13 | exhale?: number;
14 | hold1?: number;
15 | hold2?: number;
16 | inhale?: number;
17 | onCycle?: () => void;
18 | size?: number;
19 | strokeWidth?: number;
20 | }
21 |
22 | const defaultDurations = {
23 | exhale: 4000,
24 | hold1: 4000,
25 | hold2: 4000,
26 | inhale: 4000,
27 | };
28 |
29 | const easeInOutSine = (x: number): number => {
30 | return -(Math.cos(Math.PI * x) - 1) / 2;
31 | };
32 |
33 | export function App({
34 | autoStart = true,
35 | className = '',
36 | exhale = defaultDurations.exhale,
37 | hold1 = defaultDurations.hold1,
38 | hold2 = defaultDurations.hold2,
39 | inhale = defaultDurations.inhale,
40 | onCycle,
41 | size = 250,
42 | strokeWidth = 12,
43 | }: InfiniteBoxBreathingProps) {
44 | const [phase, setPhase] = useState('inhale');
45 | const [running, setRunning] = useState(autoStart);
46 | const [timeLeft, setTimeLeft] = useState(inhale);
47 | const [cycleCount, setCycleCount] = useState(0);
48 |
49 | const getPhaseDuration = useCallback(
50 | (p: Phase) => {
51 | switch (p) {
52 | case 'inhale':
53 | return inhale;
54 | case 'hold1':
55 | return hold1;
56 | case 'exhale':
57 | return exhale;
58 | case 'hold2':
59 | return hold2;
60 | }
61 | },
62 | [inhale, hold1, exhale, hold2],
63 | );
64 |
65 | const progress = 1 - timeLeft / getPhaseDuration(phase);
66 | const rafRef = useRef(null);
67 | const prevTimeRef = useRef(null);
68 | const liveRef = useRef(null);
69 |
70 | const nextPhase = (p: Phase): Phase => {
71 | switch (p) {
72 | case 'inhale':
73 | return 'hold1';
74 | case 'hold1':
75 | return 'exhale';
76 | case 'exhale':
77 | return 'hold2';
78 | case 'hold2':
79 | return 'inhale';
80 | }
81 | };
82 |
83 | const getCurrentScale = () => {
84 | const easedProgress = easeInOutSine(progress);
85 |
86 | switch (phase) {
87 | case 'inhale':
88 | return 0.5 + 0.5 * easedProgress;
89 | case 'exhale':
90 | return 1.0 - 0.5 * easedProgress;
91 | case 'hold1':
92 | return 1.0;
93 | case 'hold2':
94 | return 0.5;
95 | default:
96 | return 0.5;
97 | }
98 | };
99 |
100 | useEffect(() => {
101 | if (liveRef.current) {
102 | const secondsLeft = Math.ceil(timeLeft / 1000);
103 | liveRef.current.textContent = `${phase.replace(/\d/, '')} — ${secondsLeft}s`;
104 | }
105 | }, [timeLeft, phase]);
106 |
107 | useEffect(() => {
108 | if (!running) {
109 | if (rafRef.current) {
110 | cancelAnimationFrame(rafRef.current);
111 | rafRef.current = null;
112 | }
113 | prevTimeRef.current = null;
114 | return;
115 | }
116 |
117 | const loop = (now: number) => {
118 | if (!prevTimeRef.current) prevTimeRef.current = now;
119 | const delta = now - prevTimeRef.current;
120 | prevTimeRef.current = now;
121 |
122 | setTimeLeft(prev => {
123 | const next = Math.max(0, prev - delta);
124 | return next;
125 | });
126 |
127 | rafRef.current = requestAnimationFrame(loop);
128 | };
129 |
130 | rafRef.current = requestAnimationFrame(loop);
131 |
132 | return () => {
133 | if (rafRef.current) cancelAnimationFrame(rafRef.current);
134 | rafRef.current = null;
135 | prevTimeRef.current = null;
136 | };
137 | }, [running]);
138 |
139 | useEffect(() => {
140 | if (!running) return;
141 |
142 | if (timeLeft <= 0) {
143 | setPhase(p => {
144 | const np = nextPhase(p);
145 | if (p === 'hold2') {
146 | setCycleCount(c => c + 1);
147 |
148 | if (onCycle) onCycle();
149 | }
150 |
151 | setTimeLeft(getPhaseDuration(np));
152 | return np;
153 | });
154 | }
155 | }, [timeLeft, running, getPhaseDuration, onCycle]);
156 |
157 | useEffect(() => {
158 | setTimeLeft(getPhaseDuration(phase));
159 | }, [getPhaseDuration, phase]);
160 |
161 | const radius = (size - strokeWidth) / 2 - 2;
162 | const circumference = 2 * Math.PI * radius;
163 | const dashOffset = circumference * (1 - progress);
164 |
165 | useEffect(() => {
166 | const onKey = (e: KeyboardEvent) => {
167 | if (e.code === 'Space') {
168 | e.preventDefault();
169 | setRunning(r => !r);
170 | }
171 | };
172 | window.addEventListener('keydown', onKey);
173 | return () => window.removeEventListener('keydown', onKey);
174 | }, []);
175 |
176 | const formatSeconds = (ms: number) => `${(ms / 1000).toFixed(1)} s`;
177 |
178 | return (
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
195 |
206 |
207 |
208 |
209 |
210 |
219 |
220 |
221 |
222 |
223 | {phase.replace(/\d/, '').replace(/^(.)/, s => s.toUpperCase())}
224 |
225 |
226 |
227 |
228 | {formatSeconds(timeLeft)} Cycle {cycleCount}
229 |
230 |
231 |
232 |
233 |
234 |
235 | setRunning(prev => !prev)}>
236 | {running ? 'Pause' : 'Start'}
237 |
238 | {
240 | setRunning(false);
241 | setPhase('inhale');
242 | setTimeLeft(inhale);
243 | setCycleCount(0);
244 | }}
245 | >
246 | Reset
247 |
248 |
249 |
250 |
251 | Tip: Press Space to toggle start/pause
252 |
253 |
254 |
255 |
256 |
262 |
263 | );
264 | }
265 |
--------------------------------------------------------------------------------