134 Commits

Author SHA1 Message Date
MAZE
aa2b47ace4 chore(release): 2.4.0 2025-11-25 20:09:52 +03:30
MAZE
3a96d38a77 feat: add audio session type 2025-11-25 19:45:54 +03:30
MAZE
7e8f23f5fa chore(release): 2.3.0 2025-11-25 00:27:03 +03:30
MAZE
d0160763ee feat: turn links into buttons 2025-11-25 00:26:47 +03:30
MAZE
b921629ee3 chore: change silence 2025-11-25 00:17:52 +03:30
MAZE
ee139150f5 chore(release): 2.2.0 2025-11-25 00:10:46 +03:30
MAZE
04c52962c3 style: minor changes 2025-11-25 00:10:07 +03:30
maze
97ca030534 Merge pull request #84 from amir-rahmanii/feat/add-category-icons
feat: add category icons
2025-11-25 00:06:52 +03:30
MAZE
e160d26677 feat: replace the silence file 2025-11-24 23:56:07 +03:30
Amirreza
642a551226 feat: add category icons 2025-10-30 13:03:58 +03:30
MAZE
6ac65c1948 style: change cursor 2025-10-26 12:48:55 +03:30
MAZE
50687c97ca style: add animation on active 2025-10-26 12:43:14 +03:30
MAZE
95b641a88f feat: extract the provider for the tooltip 2025-10-25 13:11:04 +03:30
MAZE
d11a6ab062 style: increase text color 2025-08-13 12:12:27 +03:30
MAZE
a071ba04c7 style: decrease background opacity 2025-08-13 12:01:38 +03:30
MAZE
a179c09d0c style: increase line height 2025-08-13 11:59:45 +03:30
MAZE
066af9e2f3 feat: change lofi icon 2025-08-13 11:57:08 +03:30
MAZE
1e5bda707c style: change snackbar styles 2025-08-06 13:23:41 +03:30
MAZE
e2bb4dd55f style: increase border radius 2025-08-06 13:21:44 +03:30
MAZE
d9df0d4b2c feat: add shine effect 2025-08-06 13:13:14 +03:30
MAZE
3feb9c1a09 style: remove cipher animation 2025-08-06 13:08:57 +03:30
MAZE
b191e6067d feat: migrate to motion and fix some animations 2025-08-06 13:07:03 +03:30
MAZE
81d9d7ca03 feat: make sound file addresses relative 2025-07-19 22:07:06 +03:30
MAZE ✧
1e24cbc6eb Merge pull request #69 from ncguk/patch-2
Update animals.tsx
2025-07-19 21:48:24 +03:30
MAZE ✧
78fb8cd76f Merge pull request #68 from ncguk/patch-1
Rename horse-galopp.mp3 to horse-gallop.mp3
2025-07-19 21:47:50 +03:30
MAZE
4c8d577527 chore(release): 2.1.0 2025-07-19 21:16:54 +03:30
MAZE
fcbe50c78c feat: add lofi music play 2025-07-12 12:32:49 +03:30
MAZE
af096077ae fix: replace generator with static silent audio 2025-07-12 00:50:47 +03:30
MAZE
4996cc893c fix: fixate the binary pattern 2025-07-12 00:06:59 +03:30
ncguk
d6484103a7 Update animals.tsx
Change instances of "galopp" to "gallop"
2025-06-18 10:51:10 +01:00
ncguk
374de8b0d2 Rename horse-galopp.mp3 to horse-gallop.mp3
Fixes typo in filename.
2025-06-18 10:43:55 +01:00
MAZE ✧
b171793040 Merge pull request #63 from LBRDan/make-binaural-popup-preset-change-reactive
fix(component): update oscillators frequency on preset change
2025-04-05 15:36:23 +03:30
Daniele Lubrano
dcc91e038d fix(component): update oscillators frequency on preset change
Update oscillators frequency on preset change
2025-03-26 14:48:47 +01:00
MAZE
348fc1e8c4 chore: update the logo 2025-03-26 01:17:15 +03:30
MAZE
a0a7f94c33 chore: update banner 2025-03-25 23:10:51 +03:30
MAZE
2f994c6094 chore: update banner 2025-03-25 23:07:52 +03:30
MAZE
fb82117742 chore: add banner 2025-03-25 23:06:55 +03:30
MAZE
7951e9829a Merge branch 'develop' 2025-03-25 23:00:57 +03:30
MAZE
755c442263 chore: refine logo 2025-03-25 22:54:23 +03:30
MAZE
df210a1246 chore(release): 2.0.1 2025-03-25 17:54:27 +03:30
MAZE
4895a7266d fix: add delay to cipher text 2025-03-25 17:54:12 +03:30
MAZE
87f64e6574 chore(release): 2.0.0 2025-03-25 17:27:40 +03:30
MAZE
496c831552 fix: correct link 2025-03-25 17:20:09 +03:30
MAZE
c5adffb4d7 chore: comment out the banner 2025-03-25 17:19:48 +03:30
MAZE
536db4cd15 style: minor changes 2025-03-25 17:18:40 +03:30
MAZE
761c730129 feat: add PWA 2025-03-25 17:09:19 +03:30
MAZE
11e0ba2f93 style: change other assets 2025-03-25 16:42:27 +03:30
MAZE
4a92d2f1c1 style: change logo 2025-03-25 16:32:54 +03:30
MAZE
99e694161f fix: remove dropdown menu item from slider 2025-03-17 14:18:24 +03:30
MAZE
3d1d45cd49 feat: change logos 2025-03-17 13:32:24 +03:30
MAZE
309dd89a8c chore: add library sound 2025-02-18 20:14:57 +03:30
MAZE
699f49bfa3 feat: add binary animation 2025-02-14 15:20:17 +03:30
MAZE
29bebb3ec7 feat: add cipher animation 2025-02-14 15:14:53 +03:30
MAZE
7a47282165 chore: update logos 2025-02-13 20:35:40 +03:30
MAZE
2b85b276eb chore: update logos 2025-02-13 20:32:28 +03:30
MAZE ✧
0a1bf16d18 Merge pull request #54 from underoot/feature/media-controls
feat: media session support
2025-02-13 20:09:11 +03:30
MAZE
10259d013f feat: better heading 2025-02-13 20:04:10 +03:30
MAZE
e61307a302 feat: use custom slider in binaural and isochronic 2025-02-13 19:53:39 +03:30
MAZE
cb340c53a3 feat: add custom checkbox 2025-02-13 19:47:54 +03:30
MAZE
3b77c12114 feat: add custom slider 2025-02-13 19:43:21 +03:30
MAZE
b8ed79f48a feat: remove pre-made binaurals 2025-02-13 19:38:44 +03:30
Aleksandr Shoronov
d3a9f1ddba Removed Media Controls menu item 2025-01-26 10:57:51 +02:00
Aleksandr Shoronov
18ed2e6f05 feat: media session support 2025-01-18 14:06:37 +02:00
MAZE
3b829fce07 feat: add global volume 2024-10-13 21:54:52 +03:30
MAZE
e77c67bc24 fix: better implement shortcuts 2024-10-13 21:32:58 +03:30
MAZE
14c331ab6e fix: add default value 2024-09-13 19:59:29 +03:30
MAZE
5c536786ea style: add style to generators 2024-09-13 15:58:03 +03:30
MAZE
2e1fce4669 style: change icons 2024-09-13 15:28:04 +03:30
MAZE
d759064373 feat: add isochronic tone generator without styles 2024-09-13 15:17:20 +03:30
MAZE
f40e8206f8 feat: add binaural beat generator without styles 2024-09-13 14:55:04 +03:30
MAZE
d2e289e5d5 feat: add more sounds 2024-09-03 18:49:47 +03:30
MAZE
a59db41dc5 feat: change and add shortcuts 2024-09-03 18:30:24 +03:30
MAZE
554309ebd8 feat: add more sounds 2024-09-03 18:27:30 +03:30
MAZE
be38b92647 feat: add more sounds 2024-09-03 18:12:33 +03:30
MAZE
b497d16fd8 feat: add more sounds 2024-09-03 17:51:19 +03:30
MAZE
ace0d6eecc feat: add confetti 2024-09-01 13:07:19 +04:30
MAZE
aa8161aac5 feat: add done counter 2024-09-01 13:00:10 +04:30
MAZE
c6cc61a17f feat: add header to todos 2024-09-01 12:57:59 +04:30
MAZE
7f3ac26b98 style: minor changes 2024-09-01 12:48:15 +04:30
MAZE
6a4dc1ed95 feat: bring back all tools 2024-09-01 12:44:27 +04:30
MAZE
4cc85975e5 style: minor changes 2024-09-01 12:29:56 +04:30
MAZE
d42eb25f7b fix: disable the sleep timer when no sound is selected 2024-08-31 20:43:22 +03:30
MAZE
4f45279938 feat: change shortcuts 2024-08-31 20:41:22 +03:30
MAZE
105f53ea02 feat: add reverse timer 2024-08-31 19:39:31 +03:30
MAZE
f3cea66847 refactor: relocate folders 2024-08-31 19:25:51 +03:30
MAZE
a4a31dd43e refactor: remove extra hook 2024-08-31 19:22:07 +03:30
MAZE
973e0df6fb feat: remove all extra tools 2024-08-31 19:19:42 +03:30
MAZE
13d26b3337 feat: remove lofi modal 2024-08-31 19:11:58 +03:30
MAZE
e1de5c48b2 feat: bring back all tools 2024-08-31 19:05:12 +03:30
MAZE
07f37ef17f feat: add desktop notice 2024-08-31 18:22:46 +03:30
MAZE
bb39b4ba98 feat: add lofi radios 2024-08-31 18:15:24 +03:30
MAZE
76fdc74710 feat: remove the breathing exercises 2024-08-31 17:49:00 +03:30
MAZE
41845ffe5e style: remove animation on change 2024-08-31 14:34:10 +04:30
MAZE
48a85b2601 feat: add animation for labels 2024-08-31 14:02:11 +04:30
MAZE
5865fc867d feat: add timer for breathing exercises 2024-08-31 13:58:44 +04:30
MAZE
b27f24d374 style: minor changes 2024-08-31 13:26:10 +04:30
MAZE
5c9a2aa23a style: change border radius 2024-08-31 13:09:55 +04:30
MAZE
12d3255d57 style: fix pointer event 2024-08-31 13:00:04 +04:30
MAZE
c12ef12b79 style: remove pointer event 2024-08-31 12:55:55 +04:30
MAZE
ba3cd5ca5b style: add binary pattern 2024-08-31 12:49:40 +04:30
MAZE
a3b794d974 feat: add breathing exercise shortcut 2024-08-31 00:23:21 +03:30
MAZE
3ef4a076a2 fix: remove extra hook 2024-08-31 00:21:08 +03:30
MAZE
1f2b6b952c feat: add breathing exercise 2024-08-31 00:19:55 +03:30
MAZE
2bbdc7e09e feat: remove all tools 2024-08-31 00:10:58 +03:30
MAZE
47a63a774e feat: add store to the notepad 2024-08-30 23:38:37 +03:30
MAZE
edd53d8102 feat: add countdown timer 2024-08-30 23:35:58 +03:30
MAZE
302a71cdc6 style: minor change 2024-08-30 23:18:15 +03:30
MAZE
b73fd0b16e style: minor change 2024-08-30 23:17:13 +03:30
MAZE
5b3972b347 feat: add Moodist description to tools 2024-08-30 22:26:57 +03:30
MAZE
bee391acfe feat: add pomodoro timer tool 2024-08-30 22:21:23 +03:30
MAZE
1fd02f927c feat: add notepad tool page 2024-08-30 22:14:15 +03:30
MAZE
d56f8be448 refactor: refactor the breathing tool 2024-08-30 22:10:21 +03:30
MAZE
eee755378a feat: add breathing exercises and other tools 2024-08-30 22:09:07 +03:30
MAZE
4b015016e7 style: change logo color 2024-08-30 17:51:51 +03:30
MAZE
251f30930c feat: change shortcuts 2024-08-30 17:17:46 +03:30
MAZE
a29e2c20e4 style: minor changes 2024-08-30 17:14:29 +03:30
MAZE
1cf9a85e13 style: center icons 2024-08-30 17:09:07 +03:30
MAZE
69eb8832da style: add pattern 2024-08-30 17:01:30 +03:30
MAZE
c1ece582f4 feat: add new logo 2024-08-30 16:42:41 +03:30
MAZE
b32d8b2803 feat: remove all tools 2024-08-30 16:33:20 +03:30
MAZE
1768ba1548 feat: update the menu items 2024-08-30 15:30:22 +03:30
MAZE
a80289db57 feat: add notepad tool 2024-08-30 15:24:39 +03:30
MAZE
9208663050 style: change description 2024-08-30 15:20:28 +03:30
MAZE
d2edeb48be feat: add pomodoro timer 2024-08-30 15:19:35 +03:30
MAZE
27f25785e1 feat: add breathing exercises tool 2024-08-30 15:12:11 +03:30
MAZE ✧
f526f97908 Merge pull request #39 from arapl3y/feature/fix-modal-layout-shift
feat: fix modal and scrollbar layout shift
2024-08-03 21:35:17 +03:30
Alex Rapley
e399673462 feat: fix modal and scrollbar layout shift 2024-08-02 16:40:08 +10:00
MAZE
3d83a1427f perf: improve the breathing cricle 2024-07-06 19:37:30 +03:30
MAZE ✧
ddf929f4c0 Merge pull request #38 from SuperMeepBoy/accessbility-ignore-icons-screen-reader
accessibility: ignore icons for screen readers
2024-07-06 19:34:29 +03:30
MAZE
5ffb06be03 refactor: remove the timer store 2024-07-06 19:31:32 +03:30
MAZE
d6ed3fd251 feat: remove the countdown timer 2024-07-06 19:29:52 +03:30
MAZE
0052b917a8 style: reorder menu items 2024-07-04 20:02:46 +03:30
MAZE
9e38a8fd7d style: change gradient 2024-07-01 20:28:23 +03:30
Jef Roelandt
ee0a28b296 accessibility: ignore icons for screen readers 2024-06-19 23:08:39 +02:00
214 changed files with 5373 additions and 1867 deletions

View File

@@ -42,6 +42,7 @@
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"jsx-a11y/label-has-associated-control": "off",
"react/jsx-sort-props": [
"warn",
{
@@ -77,7 +78,8 @@
"rules": {
"prettier/prettier": "error",
"react/no-unknown-property": "off",
"react/jsx-key": "off"
"react/jsx-key": "off",
"react/jsx-no-undef": "off"
},
"globals": {
"Astro": "readonly"

View File

@@ -8,7 +8,8 @@
"rules": {
"import-notation": "string",
"selector-class-pattern": null
"selector-class-pattern": null,
"no-descending-specificity": null
},
"overrides": [

View File

@@ -2,6 +2,237 @@
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.
## [2.4.0](https://github.com/remvze/moodist/compare/v2.3.0...v2.4.0) (2025-11-25)
### ✨ Features
* add audio session type ([3a96d38](https://github.com/remvze/moodist/commit/3a96d38a774c7675811d5a3ea323a49d9d129bbc))
## [2.3.0](https://github.com/remvze/moodist/compare/v2.2.0...v2.3.0) (2025-11-24)
### 🚚 Chores
* change silence ([b921629](https://github.com/remvze/moodist/commit/b921629ee33c4a18a86258ba204921f732f404ff))
### ✨ Features
* turn links into buttons ([d016076](https://github.com/remvze/moodist/commit/d0160763eeb66ba47dd06098b1f2a84e234fca36))
## [2.2.0](https://github.com/remvze/moodist/compare/v2.1.0...v2.2.0) (2025-11-24)
### ✨ Features
* add category icons ([642a551](https://github.com/remvze/moodist/commit/642a5512267ce66492cf86f222fa01714960162a))
* add shine effect ([d9df0d4](https://github.com/remvze/moodist/commit/d9df0d4b2c5071c12cecc6452acc0f160c57deb5))
* change lofi icon ([066af9e](https://github.com/remvze/moodist/commit/066af9e2f31bc9201d349d888c6dc19cd5ad7750))
* extract the provider for the tooltip ([95b641a](https://github.com/remvze/moodist/commit/95b641a88f2eee264b59b5bd62206bb84119da57))
* make sound file addresses relative ([81d9d7c](https://github.com/remvze/moodist/commit/81d9d7ca03f6c7410ca750e069c9c8b935114950))
* migrate to motion and fix some animations ([b191e60](https://github.com/remvze/moodist/commit/b191e6067ddc3233689a34946c602db36d6133ba))
* replace the silence file ([e160d26](https://github.com/remvze/moodist/commit/e160d2667737b47c18b08887735be26f21bf52ae))
### 💄 Styling
* add animation on active ([50687c9](https://github.com/remvze/moodist/commit/50687c97ca483f4de3ee7633d333dfcb4def0c4d))
* change cursor ([6ac65c1](https://github.com/remvze/moodist/commit/6ac65c1948ad93fed012a8203fc8c6c2b2898b5b))
* change snackbar styles ([1e5bda7](https://github.com/remvze/moodist/commit/1e5bda707cc202407b179e2d1b95dec34bfe9420))
* decrease background opacity ([a071ba0](https://github.com/remvze/moodist/commit/a071ba04c7e86b3056049492386516b58c4210c0))
* increase border radius ([e2bb4dd](https://github.com/remvze/moodist/commit/e2bb4dd55fbf17e777ddbb6825e400bd023da328))
* increase line height ([a179c09](https://github.com/remvze/moodist/commit/a179c09d0c637d33d310960dbf3e92af4b5c526b))
* increase text color ([d11a6ab](https://github.com/remvze/moodist/commit/d11a6ab062061da5809ebddd6eb39b17c2cd3862))
* minor changes ([04c5296](https://github.com/remvze/moodist/commit/04c52962c3b65ebb7875ebadf20132846a5c020b))
* remove cipher animation ([3feb9c1](https://github.com/remvze/moodist/commit/3feb9c1a09b52a35d79cebb7ece54989e9faf481))
## [2.1.0](https://github.com/remvze/moodist/compare/v2.0.1...v2.1.0) (2025-07-19)
### 🚚 Chores
* add banner ([fb82117](https://github.com/remvze/moodist/commit/fb82117742c2a0beb8937a76fcd5f313230cd418))
* refine logo ([755c442](https://github.com/remvze/moodist/commit/755c4422635e475b8d3b0f26e3cf493a59ff3065))
* update banner ([a0a7f94](https://github.com/remvze/moodist/commit/a0a7f94c3328c65d4fc756ca52455461a05657ab))
* update banner ([2f994c6](https://github.com/remvze/moodist/commit/2f994c6094ad1948c14346badbc4462ae7782904))
* update the logo ([348fc1e](https://github.com/remvze/moodist/commit/348fc1e8c4561481e5ad1d4528e8ee480d0e2fb4))
### 🐛 Bug Fixes
* **component:** update oscillators frequency on preset change ([dcc91e0](https://github.com/remvze/moodist/commit/dcc91e038d806994382baa19b3d238da4a8ecaae))
* fixate the binary pattern ([4996cc8](https://github.com/remvze/moodist/commit/4996cc893c480ab77cf27a27801dba96771eadc5))
* replace generator with static silent audio ([af09607](https://github.com/remvze/moodist/commit/af096077aed6c42d4ff77303e6f3c1d39cd87209))
### ✨ Features
* add lofi music play ([fcbe50c](https://github.com/remvze/moodist/commit/fcbe50c78c30e4422aea2ed698fff777fcaea1c4))
### [2.0.1](https://github.com/remvze/moodist/compare/v2.0.0...v2.0.1) (2025-03-25)
### 🐛 Bug Fixes
* add delay to cipher text ([4895a72](https://github.com/remvze/moodist/commit/4895a7266d1b7458bc09a77dd6922058a247ea98))
## [2.0.0](https://github.com/remvze/moodist/compare/v1.5.1...v2.0.0) (2025-03-25)
### ✅ Testing
* add Vitest and some tests ([def9a57](https://github.com/remvze/moodist/commit/def9a57e0c6454f0e3ffd74b29153a01b33866be))
* write more tests ([9cc0ccd](https://github.com/remvze/moodist/commit/9cc0ccd325cf769d64779f133bd2d59e6ba7ca58))
* write tests for motion lib ([d356d77](https://github.com/remvze/moodist/commit/d356d77aa951b84a6ccbd0b1c6590286c042957b))
* write tests for random helper ([cad85c7](https://github.com/remvze/moodist/commit/cad85c76676cff7fe8c47ccb8d332809f7276e28))
### ⚡️ Performance Improvements
* improve the breathing cricle ([3d83a14](https://github.com/remvze/moodist/commit/3d83a1427feaec1e858953899870da06d35b4631))
### ♻️ Code Refactoring
* add description for events ([2c8135d](https://github.com/remvze/moodist/commit/2c8135db43b1a1dad789277926af0d1be3e987fc))
* add JSDoc for custom hooks ([0f50e6a](https://github.com/remvze/moodist/commit/0f50e6ae8b3d1615ed52fb168a48bbb2149090ac))
* add JSDoc for helper functions ([4ae0504](https://github.com/remvze/moodist/commit/4ae05049377506f79f5ef9f68fa7cf396d7d0528))
* change stores structure ([096251e](https://github.com/remvze/moodist/commit/096251ec0a459efbbe08d88cabab75c4ad775976))
* refactor the breathing tool ([d56f8be](https://github.com/remvze/moodist/commit/d56f8be448aa746874c38ba0cc7e00e38d339f59))
* relocate folders ([f3cea66](https://github.com/remvze/moodist/commit/f3cea668470ca06b2114a03b54660475cc560d44))
* remove extra hook ([a4a31dd](https://github.com/remvze/moodist/commit/a4a31dd43eef5c3e1d2b62cf4bb6e491e382f988))
* remove the timer store ([5ffb06b](https://github.com/remvze/moodist/commit/5ffb06be036acb1fe5d8fa4b91e4cbede39ebcc0))
* rename components ([d73b2bc](https://github.com/remvze/moodist/commit/d73b2bc1ff7689ff85c6453710b2d89927973066))
* rename stores folder ([2a86a88](https://github.com/remvze/moodist/commit/2a86a88ed6a232c4a8c2a10bbb06f586361f732d))
* separate the migration ([c35409c](https://github.com/remvze/moodist/commit/c35409ce0a95d8376f0d84c96ed0975c9f3a1301))
* use the ID instead of index ([7658842](https://github.com/remvze/moodist/commit/7658842324a92210a6a612c70c5479c6bb7f3c05))
* write JSDoc for libs ([fddf75c](https://github.com/remvze/moodist/commit/fddf75cdca1f121160f9054c82a7a1ddedd6f2fa))
### ✨ Features
* add active indicator for sleep timer ([82d8240](https://github.com/remvze/moodist/commit/82d8240b9708a9d522f67ae305dc44e004ced6de))
* add animation for labels ([48a85b2](https://github.com/remvze/moodist/commit/48a85b26016a8f3cc934e1b2298b0d897ffd9b43))
* add basic form ([c272914](https://github.com/remvze/moodist/commit/c27291441625eb6528b28f55af3f88e1debd8a55))
* add binary animation ([699f49b](https://github.com/remvze/moodist/commit/699f49bfa33420698962b56db23b49c8e14bb354))
* add binaural beat generator without styles ([f40e820](https://github.com/remvze/moodist/commit/f40e8206f8126f1988e0e39ca522ac3c5eb8139f))
* add breathing exercise ([1f2b6b9](https://github.com/remvze/moodist/commit/1f2b6b952c65c04828f19506134d783a7491df23))
* add breathing exercise shortcut ([a3b794d](https://github.com/remvze/moodist/commit/a3b794d9748d4a9877e5727269178f207fbc03d5))
* add breathing exercises and other tools ([eee7553](https://github.com/remvze/moodist/commit/eee755378a14d93d1363e8c265a908d50b9cc332))
* add breathing exercises tool ([27f2578](https://github.com/remvze/moodist/commit/27f25785e1cfc0482d7ddd625ac1219fd5bb6863))
* add cipher animation ([29bebb3](https://github.com/remvze/moodist/commit/29bebb3ec74d969fb42968696e470db00a07766e))
* add confetti ([ace0d6e](https://github.com/remvze/moodist/commit/ace0d6eeccc65c96275a24c8a96e63988cf76134))
* add countdown timer ([edd53d8](https://github.com/remvze/moodist/commit/edd53d8102871d53b0a11eaa9bae7323f874d988))
* add countdown timer structure ([c5657d0](https://github.com/remvze/moodist/commit/c5657d06425aea84a4ba9a4b2f48e312be8b0271))
* add custom checkbox ([cb340c5](https://github.com/remvze/moodist/commit/cb340c53a39917722137a8ee05b779af04a1203d))
* add custom slider ([3b77c12](https://github.com/remvze/moodist/commit/3b77c12114e5e37c0a3a17c945a0e69e034a35a4))
* add desktop notice ([07f37ef](https://github.com/remvze/moodist/commit/07f37ef17f8be893d3ceba8fbe4427a9ecda5c15))
* add done counter ([aa8161a](https://github.com/remvze/moodist/commit/aa8161aac5eb238048c713500a091e9af1c98e6a))
* add global volume ([3b829fc](https://github.com/remvze/moodist/commit/3b829fce07ed7adf11ca9993c33e33caab285763))
* add header to todos ([c6cc61a](https://github.com/remvze/moodist/commit/c6cc61a17fcb8542ece3caccc0de536d8003b106))
* add ID to presets ([78222be](https://github.com/remvze/moodist/commit/78222be011cf93998faed0b7926a5b49dcdeb470))
* add isochronic tone generator without styles ([d759064](https://github.com/remvze/moodist/commit/d759064373fe791f641db39549e05341068ae8a2))
* add lofi radios ([bb39b4b](https://github.com/remvze/moodist/commit/bb39b4ba98f20da13e1e7a440441f5474a823f32))
* add Moodist description to tools ([5b3972b](https://github.com/remvze/moodist/commit/5b3972b3470f3c43903d9a20925ed49321f07440))
* add more sounds ([d2e289e](https://github.com/remvze/moodist/commit/d2e289e5d5cccd050ca94860f05f00740b3cf139))
* add more sounds ([554309e](https://github.com/remvze/moodist/commit/554309ebd87da2bce4555f09e5c9f34735d0b794))
* add more sounds ([be38b92](https://github.com/remvze/moodist/commit/be38b92647209ce17032987b3d6f5d1800322db5))
* add more sounds ([b497d16](https://github.com/remvze/moodist/commit/b497d16fd8b7d6ccf34c0c91b596fca75dff2f34))
* add move up and down functionality ([3e11fb6](https://github.com/remvze/moodist/commit/3e11fb6123e4c6b6be9668ef4c274390a5acd16a))
* add new logo ([c1ece58](https://github.com/remvze/moodist/commit/c1ece582f445906308a0d856181ebaca464ec25a))
* add notepad tool ([a80289d](https://github.com/remvze/moodist/commit/a80289db57c1b002edd586b323444d3a474587ad))
* add notepad tool page ([1fd02f9](https://github.com/remvze/moodist/commit/1fd02f927c55155ecd8d1af6325995c4635e0a29))
* add persist mode to the modal ([4c0f417](https://github.com/remvze/moodist/commit/4c0f417469fb15adbe33cab9bb66459225653e68))
* add pomodoro timer ([d2edeb4](https://github.com/remvze/moodist/commit/d2edeb48becef62f1002359a41ebe8ebfa1f34bb))
* add pomodoro timer tool ([bee391a](https://github.com/remvze/moodist/commit/bee391acfecdaf36488c48ef1022b16a83059d58))
* add PWA ([761c730](https://github.com/remvze/moodist/commit/761c7301295a3e5645326be804225431f823f808))
* add reverse timer ([105f53e](https://github.com/remvze/moodist/commit/105f53ea028fadae4bd2ff7d8a1856e94f070b1a))
* add shortcut for breathing exercise ([60cb453](https://github.com/remvze/moodist/commit/60cb453847f0968a4d1abc0fbb66773a54ebdfd9))
* add simple breathing exercise tool ([fc4f521](https://github.com/remvze/moodist/commit/fc4f52146e2142a0c711b6d6a334c0107b1e1daa))
* add store to the notepad ([47a63a7](https://github.com/remvze/moodist/commit/47a63a774ebede5db65f17a29a36f0b76d9ed85a))
* add timer for breathing exercises ([5865fc8](https://github.com/remvze/moodist/commit/5865fc867dc97e03d0f0c79ea8c465e0c0f27411))
* better heading ([10259d0](https://github.com/remvze/moodist/commit/10259d013f7cb1ae41808f7a78e836ddee3b07f1))
* bring back all tools ([6a4dc1e](https://github.com/remvze/moodist/commit/6a4dc1ed95072c402cb553fa5b1becb646062c45))
* bring back all tools ([e1de5c4](https://github.com/remvze/moodist/commit/e1de5c48b299e815f071f15c00424ba1b0189419))
* change and add shortcuts ([a59db41](https://github.com/remvze/moodist/commit/a59db41dc5eaa7be5ab86c5cc407274eb7b57dfe))
* change logos ([3d1d45c](https://github.com/remvze/moodist/commit/3d1d45cd4933335cfbe20381c0e758969a3bdcb9))
* change shortcuts ([4f45279](https://github.com/remvze/moodist/commit/4f45279938f60ee6934c3e6047898b9833c2b9c6))
* change shortcuts ([251f309](https://github.com/remvze/moodist/commit/251f30930c72a50120412c6b2182fdf4183b9d62))
* fix modal and scrollbar layout shift ([e399673](https://github.com/remvze/moodist/commit/e3996734621b33c0598db29e82371f1258396147))
* implement countdown timer functionality ([2bfb9b1](https://github.com/remvze/moodist/commit/2bfb9b181c490c9836e2410199e6a1cf8687e7aa))
* media session support ([18ed2e6](https://github.com/remvze/moodist/commit/18ed2e6f055d7e32b4a9df33cdb724eaf1f930aa))
* remove all extra tools ([973e0df](https://github.com/remvze/moodist/commit/973e0df6fb3a6749fd4b0f8d1cd976c67a7e8c43))
* remove all tools ([2bbdc7e](https://github.com/remvze/moodist/commit/2bbdc7e09e053bd6e8bb052abb7aff723cb14eaa))
* remove all tools ([b32d8b2](https://github.com/remvze/moodist/commit/b32d8b28034e018eeaf1c544e4128b91f4a95172))
* remove lofi modal ([13d26b3](https://github.com/remvze/moodist/commit/13d26b3337b2e79d52c774807795b5924a4dcb76))
* remove pre-made binaurals ([b8ed79f](https://github.com/remvze/moodist/commit/b8ed79f48ad2a315b93aedf1f932b6c5f075b157))
* remove the breathing exercises ([76fdc74](https://github.com/remvze/moodist/commit/76fdc747100bc15ced92b77b1fefc8cba519d37f))
* remove the countdown timer ([d6ed3fd](https://github.com/remvze/moodist/commit/d6ed3fd251df029100caba5df304996e723acd78))
* replace reverse timer ([a6c7ac4](https://github.com/remvze/moodist/commit/a6c7ac41ad5210b9a98e0fe62f5cb387fe9c4e9a))
* scroll into view after marking favorite ([74f6b58](https://github.com/remvze/moodist/commit/74f6b5851d3a0fac5f97d97cd24f12507c2c3b35))
* scroll the new timer into view ([f4c66e3](https://github.com/remvze/moodist/commit/f4c66e309277414951b191e627b1f52aab79af6f))
* update the menu items ([1768ba1](https://github.com/remvze/moodist/commit/1768ba1548a444c57dbfd5e351d77838238aed0d))
* use custom slider in binaural and isochronic ([e61307a](https://github.com/remvze/moodist/commit/e61307a30263dca8cc016ec5136d52c4b18e5c3c))
### 💄 Styling
* add animation to presets ([787a9b6](https://github.com/remvze/moodist/commit/787a9b60b51334ec2a7423d489f71c305661039e))
* add binary pattern ([ba3cd5c](https://github.com/remvze/moodist/commit/ba3cd5ca5be8435f32b93d5a499e37388340bff8))
* add focus state ([af075b3](https://github.com/remvze/moodist/commit/af075b32e64a6ab923d60282558250b79cc12da3))
* add min width ([18987cc](https://github.com/remvze/moodist/commit/18987cc33997c7b010aea2d4f1546ddcabe1a46b))
* add pattern ([69eb883](https://github.com/remvze/moodist/commit/69eb8832dae026706f76ba21a74fcb248ba4309d))
* add style to generators ([5c53678](https://github.com/remvze/moodist/commit/5c536786ea64e9722a67289ab2d7e56e7a259404))
* add title to timer ([a3c384d](https://github.com/remvze/moodist/commit/a3c384d1054b81e056265eecd9344496c9b0b5ce))
* center icons ([1cf9a85](https://github.com/remvze/moodist/commit/1cf9a85e13d50d3c5335dfb78fa57543ce6fda44))
* change border radius ([5c9a2aa](https://github.com/remvze/moodist/commit/5c9a2aa23aa04f9386e7d7ac9a20759a2ed87acc))
* change button style ([8a79ccf](https://github.com/remvze/moodist/commit/8a79ccf018cd7ee86b27b8bd187975376abea953))
* change description ([9208663](https://github.com/remvze/moodist/commit/9208663050c340fdecf486b4835d30353852fd22))
* change gradient ([9e38a8f](https://github.com/remvze/moodist/commit/9e38a8fd7da2d68c8c04c4c21cbda6444e9e247b))
* change icons ([2e1fce4](https://github.com/remvze/moodist/commit/2e1fce46695b693c4b6aa11f18506e2f2cd9bb59))
* change item order ([9198315](https://github.com/remvze/moodist/commit/919831538fea639eb60c8fb84fa93a79ec2cd9c5))
* change logo ([4a92d2f](https://github.com/remvze/moodist/commit/4a92d2f1c12c12b4166500149937be51e6442f71))
* change logo color ([4b01501](https://github.com/remvze/moodist/commit/4b015016e7c531afc3f3b1f51d62bf96232e3ea8))
* change notice ([9d1d8f8](https://github.com/remvze/moodist/commit/9d1d8f80359097b9122673564d3d57c0827ff3db))
* change other assets ([11e0ba2](https://github.com/remvze/moodist/commit/11e0ba2f938fc08984e4acba1ba6b4ac3239cacf))
* fix pointer event ([12d3255](https://github.com/remvze/moodist/commit/12d3255d57083ff72ae919b6161922620dc1d6e2))
* increase menu width ([96ca376](https://github.com/remvze/moodist/commit/96ca3768856806bbe761e773d5ef626dcd12c968))
* minor change ([302a71c](https://github.com/remvze/moodist/commit/302a71cdc6472dd29d75372ddc6a3ef214dd68c4))
* minor change ([b73fd0b](https://github.com/remvze/moodist/commit/b73fd0b16e57140350d0743aa98ec6933bdc5c64))
* minor changes ([536db4c](https://github.com/remvze/moodist/commit/536db4cd156cb391a0b1ef9bf3e4fbbac06ccc11))
* minor changes ([7f3ac26](https://github.com/remvze/moodist/commit/7f3ac26b982e629eef891f706004eca5f14e11c4))
* minor changes ([4cc8597](https://github.com/remvze/moodist/commit/4cc85975e54cfd8195596e017c351a227184806b))
* minor changes ([b27f24d](https://github.com/remvze/moodist/commit/b27f24d37484a04495a043170ccaf4b4923b31ac))
* minor changes ([a29e2c2](https://github.com/remvze/moodist/commit/a29e2c20e4bac276495b409b20a6ffaa079122e2))
* remove animation on change ([41845ff](https://github.com/remvze/moodist/commit/41845ffe5e282c07b3c4cdea56607f1668c636bd))
* remove animations ([28abc16](https://github.com/remvze/moodist/commit/28abc16b9cbbc3986f7fb3feb17e57e553cda5dd))
* remove pointer event ([c12ef12](https://github.com/remvze/moodist/commit/c12ef12b79c6db93c457b77f4bfccb2848dc8067))
* reorder menu items ([0052b91](https://github.com/remvze/moodist/commit/0052b917a817ca7f83fe23521077d99ae78e81cd))
### 🚚 Chores
* add animation to countdown timer ([73a5c21](https://github.com/remvze/moodist/commit/73a5c21be918e1e105214078eaef8d76b168333b))
* add library sound ([309dd89](https://github.com/remvze/moodist/commit/309dd89a8c13eb2647217c81d7fc0a82eb3ebaae))
* add toolbox copy ([cfd2744](https://github.com/remvze/moodist/commit/cfd2744e92b7a2948597a750275bf9c900248d55))
* comment out the banner ([c5adffb](https://github.com/remvze/moodist/commit/c5adffb4d777eda1e2a092e382c1cac616dd60f1))
* update logos ([7a47282](https://github.com/remvze/moodist/commit/7a472821652d1359126568836b3040ce1fa454c5))
* update logos ([2b85b27](https://github.com/remvze/moodist/commit/2b85b276eb11d862bf1abd1e6f099740d9b85c10))
### 🐛 Bug Fixes
* add default value ([14c331a](https://github.com/remvze/moodist/commit/14c331ab6e692ea3fcdaa056e32728f0a1cd2772))
* better implement shortcuts ([e77c67b](https://github.com/remvze/moodist/commit/e77c67bc24f1831bb6de80a4335c51e5b84009ed))
* change icon path ([09c0a6c](https://github.com/remvze/moodist/commit/09c0a6ce93f8b0f62149928218532201e0de16c5))
* change shortcuts ([edd15f4](https://github.com/remvze/moodist/commit/edd15f4b9a0291b9794102fbb41048de10b0fd69))
* correct link ([496c831](https://github.com/remvze/moodist/commit/496c831552442047d5556376a212698c8931b698))
* disable the sleep timer when no sound is selected ([d42eb25](https://github.com/remvze/moodist/commit/d42eb25f7be64b5e77cd0bacd1538949d331aff7))
* icons path ([1a1359c](https://github.com/remvze/moodist/commit/1a1359c989268a22cfdba20f198af192726ac2ce))
* remove dropdown menu item from slider ([99e6941](https://github.com/remvze/moodist/commit/99e694161f16a3be03cbda0854687a244df42f21))
* remove extra hook ([3ef4a07](https://github.com/remvze/moodist/commit/3ef4a076a2b48911d37f75067dc60ea15dd28405))
### [1.5.1](https://github.com/remvze/moodist/compare/v1.5.0...v1.5.1) (2024-06-14)

View File

@@ -1,8 +1,8 @@
<div align="center">
<img src="/assets/banner.svg" alt="Moodist Logo Banner" />
<img src="/assets/banner.png" alt="Moodist Logo Banner" />
<h2>Moodist 🌲</h2>
<p>Ambient sounds for focus and calm.</p>
<a href="https://moodist.app">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
<a href="https://moodist.mvze.net">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
</div>
## Table of Contents
@@ -24,6 +24,7 @@
1. 📓 Notepad for quick notes.
1. 🍅 Pomodoro timer.
1. ✅ Simple to-do list (soon).
1. ⏯️ Media controls.
1. ⌨️ Keyboard shortcuts for everything.
1. 🥷 Privacy focused: no data collection.
1. 💰 Completely free, open-source, and self-hostable.

BIN
assets/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -1,40 +0,0 @@
<svg width="1200" height="400" viewBox="0 0 1200 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="400" rx="25" fill="#09090B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M600 237.5C620.711 237.5 637.5 220.711 637.5 200C637.5 179.289 620.711 162.5 600 162.5C579.289 162.5 562.5 179.289 562.5 200C562.5 220.711 579.289 237.5 600 237.5ZM600 218.75C610.355 218.75 618.75 210.355 618.75 200C618.75 189.645 610.355 181.25 600 181.25C589.645 181.25 581.25 189.645 581.25 200C581.25 210.355 589.645 218.75 600 218.75Z" fill="#FAFAFA"/>
<path d="M562.5 162.5C541.789 162.5 525 179.289 525 200C525 220.711 541.789 237.5 562.5 237.5L562.5 218.75C552.145 218.75 543.75 210.355 543.75 200C543.75 189.645 552.145 181.25 562.5 181.25L562.5 162.5Z" fill="#D4D4D8"/>
<path d="M637.5 162.5C637.5 141.789 620.711 125 600 125C579.289 125 562.5 141.789 562.5 162.5L581.25 162.5C581.25 152.145 589.645 143.75 600 143.75C610.355 143.75 618.75 152.145 618.75 162.5L637.5 162.5Z" fill="#D4D4D8"/>
<path d="M637.5 237.5C658.211 237.5 675 220.711 675 200C675 179.289 658.211 162.5 637.5 162.5L637.5 181.25C647.855 181.25 656.25 189.645 656.25 200C656.25 210.355 647.855 218.75 637.5 218.75L637.5 237.5Z" fill="#D4D4D8"/>
<path d="M562.5 237.5C562.5 258.211 579.289 275 600 275C620.711 275 637.5 258.211 637.5 237.5H618.75C618.75 247.855 610.355 256.25 600 256.25C589.645 256.25 581.25 247.855 581.25 237.5H562.5Z" fill="#D4D4D8"/>
<path d="M543.75 162.5C543.75 152.145 552.145 143.75 562.5 143.75L562.5 125C541.789 125 525 141.789 525 162.5L543.75 162.5Z" fill="#A1A1AA"/>
<path d="M637.5 143.75C647.855 143.75 656.25 152.145 656.25 162.5L675 162.5C675 141.789 658.211 125 637.5 125L637.5 143.75Z" fill="#A1A1AA"/>
<path d="M656.25 237.5C656.25 247.855 647.855 256.25 637.5 256.25L637.5 275C658.211 275 675 258.211 675 237.5L656.25 237.5Z" fill="#A1A1AA"/>
<path d="M562.5 256.25C552.145 256.25 543.75 247.855 543.75 237.5H525C525 258.211 541.789 275 562.5 275V256.25Z" fill="#A1A1AA"/>
<path d="M693.75 237.5C693.75 247.855 685.355 256.25 675 256.25L675 275C695.711 275 712.5 258.211 712.5 237.5L693.75 237.5Z" fill="#18181B"/>
<path d="M656.25 275C656.25 285.355 647.855 293.75 637.5 293.75L637.5 312.5C658.211 312.5 675 295.711 675 275L656.25 275Z" fill="#18181B"/>
<path d="M525 256.25C514.645 256.25 506.25 247.855 506.25 237.5H487.5C487.5 258.211 504.289 275 525 275V256.25Z" fill="#18181B"/>
<path d="M562.5 293.75C552.145 293.75 543.75 285.355 543.75 275H525C525 295.711 541.789 312.5 562.5 312.5V293.75Z" fill="#18181B"/>
<path d="M562.5 331.25C552.145 331.25 543.75 322.855 543.75 312.5H525C525 333.211 541.789 350 562.5 350V331.25Z" fill="#18181B"/>
<path d="M525 293.75C514.645 293.75 506.25 285.355 506.25 275H487.5C487.5 295.711 504.289 312.5 525 312.5V293.75Z" fill="#18181B"/>
<path d="M487.5 256.25C477.145 256.25 468.75 247.855 468.75 237.5H450C450 258.211 466.789 275 487.5 275V256.25Z" fill="#18181B"/>
<path d="M543.75 125C543.75 114.645 552.145 106.25 562.5 106.25L562.5 87.5C541.789 87.5 525 104.289 525 125L543.75 125Z" fill="#18181B"/>
<path d="M506.25 162.5C506.25 152.145 514.645 143.75 525 143.75L525 125C504.289 125 487.5 141.789 487.5 162.5L506.25 162.5Z" fill="#18181B"/>
<path d="M468.75 162.5C468.75 152.145 477.145 143.75 487.5 143.75L487.5 125C466.789 125 450 141.789 450 162.5L468.75 162.5Z" fill="#18181B"/>
<path d="M506.25 125C506.25 114.645 514.645 106.25 525 106.25L525 87.5C504.289 87.5 487.5 104.289 487.5 125L506.25 125Z" fill="#18181B"/>
<path d="M543.75 87.5C543.75 77.1447 552.145 68.75 562.5 68.75L562.5 50C541.789 50 525 66.7893 525 87.5L543.75 87.5Z" fill="#18181B"/>
<path d="M675 143.75C685.355 143.75 693.75 152.145 693.75 162.5L712.5 162.5C712.5 141.789 695.711 125 675 125L675 143.75Z" fill="#18181B"/>
<path d="M637.5 106.25C647.855 106.25 656.25 114.645 656.25 125L675 125C675 104.289 658.211 87.5 637.5 87.5L637.5 106.25Z" fill="#18181B"/>
<path d="M637.5 68.75C647.855 68.75 656.25 77.1447 656.25 87.5L675 87.5C675 66.7893 658.211 50 637.5 50L637.5 68.75Z" fill="#18181B"/>
<path d="M675 106.25C685.355 106.25 693.75 114.645 693.75 125L712.5 125C712.5 104.289 695.711 87.5 675 87.5L675 106.25Z" fill="#18181B"/>
<path d="M712.5 143.75C722.855 143.75 731.25 152.145 731.25 162.5L750 162.5C750 141.789 733.211 125 712.5 125L712.5 143.75Z" fill="#18181B"/>
<path d="M693.75 275C693.75 285.355 685.355 293.75 675 293.75L675 312.5C695.711 312.5 712.5 295.711 712.5 275L693.75 275Z" fill="#18181B"/>
<path d="M731.25 237.5C731.25 247.855 722.855 256.25 712.5 256.25L712.5 275C733.211 275 750 258.211 750 237.5L731.25 237.5Z" fill="#18181B"/>
<path d="M656.25 312.5C656.25 322.855 647.855 331.25 637.5 331.25L637.5 350C658.211 350 675 333.211 675 312.5L656.25 312.5Z" fill="#18181B"/>
<path d="M525 162.5C504.289 162.5 487.5 179.289 487.5 200C487.5 220.711 504.289 237.5 525 237.5L525 218.75C514.645 218.75 506.25 210.355 506.25 200C506.25 189.645 514.645 181.25 525 181.25L525 162.5Z" fill="#18181B"/>
<path d="M487.5 162.5C466.789 162.5 450 179.289 450 200C450 220.711 466.789 237.5 487.5 237.5L487.5 218.75C477.145 218.75 468.75 210.355 468.75 200C468.75 189.645 477.145 181.25 487.5 181.25L487.5 162.5Z" fill="#18181B"/>
<path d="M637.5 125C637.5 104.289 620.711 87.5 600 87.5C579.289 87.5 562.5 104.289 562.5 125L581.25 125C581.25 114.645 589.645 106.25 600 106.25C610.355 106.25 618.75 114.645 618.75 125L637.5 125Z" fill="#18181B"/>
<path d="M637.5 87.5C637.5 66.7893 620.711 50 600 50C579.289 50 562.5 66.7893 562.5 87.5L581.25 87.5C581.25 77.1447 589.645 68.75 600 68.75C610.355 68.75 618.75 77.1447 618.75 87.5L637.5 87.5Z" fill="#18181B"/>
<path d="M675 237.5C695.711 237.5 712.5 220.711 712.5 200C712.5 179.289 695.711 162.5 675 162.5L675 181.25C685.355 181.25 693.75 189.645 693.75 200C693.75 210.355 685.355 218.75 675 218.75L675 237.5Z" fill="#18181B"/>
<path d="M712.5 237.5C733.211 237.5 750 220.711 750 200C750 179.289 733.211 162.5 712.5 162.5L712.5 181.25C722.855 181.25 731.25 189.645 731.25 200C731.25 210.355 722.855 218.75 712.5 218.75L712.5 237.5Z" fill="#18181B"/>
<path d="M562.5 275C562.5 295.711 579.289 312.5 600 312.5C620.711 312.5 637.5 295.711 637.5 275H618.75C618.75 285.355 610.355 293.75 600 293.75C589.645 293.75 581.25 285.355 581.25 275H562.5Z" fill="#18181B"/>
<path d="M562.5 312.5C562.5 333.211 579.289 350 600 350C620.711 350 637.5 333.211 637.5 312.5H618.75C618.75 322.855 610.355 331.25 600 331.25C589.645 331.25 581.25 322.855 581.25 312.5H562.5Z" fill="#18181B"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,7 +1,36 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import AstroPWA from '@vite-pwa/astro';
export default defineConfig({
integrations: [react()],
integrations: [
react(),
AstroPWA({
manifest: {
background_color: '#09090b',
description: 'Ambient sounds for focus and calm.',
display: 'standalone',
icons: [
...[72, 128, 144, 152, 192, 256, 512].map(size => ({
sizes: `${size}x${size}`,
src: `/assets/pwa/${size}.png`,
type: 'image/png',
})),
],
name: 'Moodist',
orientation: 'any',
scope: '/',
short_name: 'Moodist',
start_url: '/',
theme_color: '#09090b',
},
registerType: 'prompt',
workbox: {
globPatterns: ['**/*'],
maximumFileSizeToCacheInBytes: Number.MAX_SAFE_INTEGER,
navigateFallback: '/',
},
}),
],
});

2100
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "moodist",
"type": "module",
"version": "1.5.1",
"version": "2.4.0",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
@@ -27,21 +27,27 @@
"@astrojs/react": "3.6.0",
"@floating-ui/react": "0.26.0",
"@formkit/auto-animate": "0.8.2",
"@radix-ui/react-checkbox": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.0.6",
"@radix-ui/react-tooltip": "1.0.7",
"@radix-ui/react-slider": "1.2.3",
"@radix-ui/react-tooltip": "1.2.8",
"@types/howler": "2.2.10",
"@types/react": "^18.2.25",
"@types/react-dom": "^18.2.10",
"@vite-pwa/astro": "0.5.0",
"astro": "4.10.3",
"deepmerge": "4.3.1",
"focus-trap-react": "10.2.3",
"framer-motion": "10.16.4",
"howler": "2.2.4",
"js-confetti": "0.12.0",
"motion": "12.23.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "3.2.1",
"react-icons": "4.11.0",
"react-wrap-balancer": "1.1.0",
"react-youtube": "10.1.0",
"uuid": "10.0.0",
"zustand": "4.4.3"
},

BIN
public/assets/pwa/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/assets/pwa/144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/assets/pwa/152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/assets/pwa/192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
public/assets/pwa/256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
public/assets/pwa/512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/assets/pwa/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logo-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
public/logo-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/sounds/silence.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -18,10 +18,6 @@ const paragraphs = [
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
title: 'Create Your Soundscape',
},
{
body: 'Moodist offers more than ambient sounds with its suite of productivity tools to keep you organized and focused. Use the built-in pomodoro timer for structured work intervals, jot down ideas in the notepad, track tasks with the to-do list (coming soon), and set multiple timers with the distraction-free countdown timer. These tools integrate seamlessly with the ambient soundscapes, creating a personalized environment that fosters focus and relaxation.',
title: 'A Productivity Toolbox',
},
{
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
title: 'Sounds for Every Moment',

View File

@@ -12,6 +12,7 @@ import { Categories } from '@/components/categories';
import { SharedModal } from '@/components/modals/shared';
import { Toolbar } from '@/components/toolbar';
import { SnackbarProvider } from '@/contexts/snackbar';
import { MediaControls } from '@/components/media-controls';
import { sounds } from '@/data/sounds';
import { FADE_OUT } from '@/constants/events';
@@ -88,6 +89,7 @@ export function App() {
return (
<SnackbarProvider>
<StoreConsumer>
<MediaControls />
<Container>
<div id="app" />
<Buttons />

View File

@@ -0,0 +1,7 @@
---
import { generateRandomBinaryString } from '@/helpers/binary';
const binary = generateRandomBinaryString(1000);
---
<span>{binary}</span>

View File

@@ -18,6 +18,10 @@
background-color: var(--color-neutral-800);
}
&:not(.disabled):active {
transform: scale(0.97);
}
&:disabled,
&.disabled {
cursor: not-allowed;

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect } from 'react';
import { BiPause, BiPlay } from 'react-icons/bi/index';
import { useHotkeys } from 'react-hotkeys-hook';
import { useSoundStore } from '@/stores/sound';
import { useSnackbar } from '@/contexts/snackbar';
@@ -28,17 +29,7 @@ export function PlayButton() {
if (isPlaying && noSelected) pause();
}, [isPlaying, pause, noSelected]);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.shiftKey && e.key === ' ') {
handleToggle();
}
};
document.addEventListener('keydown', listener);
return () => document.removeEventListener('keydown', listener);
}, [handleToggle]);
useHotkeys('shift+space', handleToggle, {}, [handleToggle]);
return (
<button
@@ -48,14 +39,14 @@ export function PlayButton() {
>
{isPlaying ? (
<>
<span>
<span aria-hidden="true">
<BiPause />
</span>{' '}
Pause
</>
) : (
<>
<span>
<span aria-hidden="true">
<BiPlay />
</span>{' '}
Play

View File

@@ -19,6 +19,10 @@
cursor: not-allowed;
}
&:active {
transform: scale(0.97);
}
&:hover,
&:focus-visible {
background-color: var(--color-neutral-200);

View File

@@ -1,6 +1,7 @@
import { useEffect, useCallback } from 'react';
import { useCallback } from 'react';
import { BiUndo, BiTrash } from 'react-icons/bi/index';
import { AnimatePresence, motion } from 'framer-motion';
import { AnimatePresence, motion } from 'motion/react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Tooltip } from '@/components/tooltip';
@@ -28,17 +29,7 @@ export function UnselectButton() {
else if (!noSelected) unselectAll(true);
}, [hasHistory, noSelected, unselectAll, restoreHistory, locked]);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.shiftKey && e.key === 'R') {
handleToggle();
}
};
document.addEventListener('keydown', listener);
return () => document.removeEventListener('keydown', listener);
}, [handleToggle]);
useHotkeys('shift+r', handleToggle, {}, [handleToggle]);
return (
<>
@@ -50,30 +41,31 @@ export function UnselectButton() {
initial="hidden"
variants={variants}
>
<Tooltip
showDelay={0}
content={
hasHistory
? 'Restore unselected sounds.'
: 'Unselect all sounds.'
}
>
<button
disabled={noSelected && !hasHistory}
aria-label={
<Tooltip.Provider delayDuration={0}>
<Tooltip
content={
hasHistory
? 'Restore Unselected Sounds'
: 'Unselect All Sounds'
? 'Restore unselected sounds.'
: 'Unselect all sounds.'
}
className={cn(
styles.unselectButton,
noSelected && !hasHistory && styles.disabled,
)}
onClick={handleToggle}
>
{hasHistory ? <BiUndo /> : <BiTrash />}
</button>
</Tooltip>
<button
disabled={noSelected && !hasHistory}
aria-label={
hasHistory
? 'Restore Unselected Sounds'
: 'Unselect All Sounds'
}
className={cn(
styles.unselectButton,
noSelected && !hasHistory && styles.disabled,
)}
onClick={handleToggle}
>
{hasHistory ? <BiUndo /> : <BiTrash />}
</button>
</Tooltip>
</Tooltip.Provider>
</motion.div>
)}
</AnimatePresence>

View File

@@ -1,6 +1,6 @@
import { AnimatePresence } from 'framer-motion';
import { AnimatePresence } from 'motion/react';
import { Category } from '@/components/category';
import { Category } from './category';
import { Donate } from './donate';
import type { Categories } from '@/data/types';

View File

@@ -0,0 +1,47 @@
.wrapper {
display: flex;
flex-direction: column;
gap: 20px;
padding-bottom: 80px;
& .title {
font-family: var(--font-display);
font-size: var(--font-lg);
font-weight: 600;
text-align: center;
}
& .categoryIconsWrapper {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
justify-content: center;
& .icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
font-size: var(--font-md);
color: var(--color-foreground);
cursor: pointer;
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
transition: 0.2s;
&:hover {
background: linear-gradient(
var(--color-neutral-100),
var(--color-neutral-200)
);
transform: scale(1.15);
}
}
}
}

View File

@@ -0,0 +1,42 @@
import { sounds } from '@/data/sounds';
import { useMemo } from 'react';
import styles from './category-icons.module.css';
import { Container } from '@/components/container';
import { Tooltip } from '@/components/tooltip';
export default function CategoryIcons() {
const categories = useMemo(() => sounds.categories, []);
const goto = (id: string) => {
const category = document.getElementById(`category-${id}`);
category?.scrollIntoView();
};
return (
<Container>
<div className={styles.wrapper}>
<h3 className={styles.title}>Categories</h3>
<div className={styles.categoryIconsWrapper}>
<Tooltip.Provider delayDuration={0}>
{categories.map(category => {
return (
<Tooltip
content={category.title}
key={category.id}
placement="bottom"
>
<button
className={styles.icon}
onClick={() => goto(category.id)}
>
{category.icon}
</button>
</Tooltip>
);
})}
</Tooltip.Provider>
</div>
</div>
</Container>
);
}

View File

@@ -22,7 +22,10 @@
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}

View File

@@ -19,7 +19,9 @@ export function Category({
<div className={styles.category} id={`category-${id}`}>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>{icon}</div>
<div aria-hidden="true" className={styles.icon}>
{icon}
</div>
</div>
<div className={styles.title}>{title}</div>

View File

@@ -20,7 +20,10 @@
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}
@@ -31,6 +34,16 @@
font-size: var(--font-lg);
font-weight: 600;
text-align: center;
& span {
background: linear-gradient(
135deg,
var(--color-foreground),
var(--color-foreground-subtle)
);
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
& .desc {

View File

@@ -9,12 +9,14 @@ export function Donate() {
<div className={styles.donate}>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>
<div aria-hidden="true" className={styles.icon}>
<FaCoffee />
</div>
</div>
<div className={styles.title}>Support Me</div>
<div className={styles.title}>
<span>Support Me</span>
</div>
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
<SpecialButton
className={styles.button}

View File

@@ -0,0 +1,23 @@
.checkboxRoot {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
background: var(--color-neutral-100);
border: 2px solid var(--color-neutral-300);
border-radius: 4px;
transition: 0.2s;
}
.checkboxRoot[data-state='checked'] {
background: var(--color-neutral-950);
border: 2px solid var(--color-neutral-950);
}
.checkboxIndicator {
font-size: var(--font-2xsm);
color: var(--color-neutral-50);
transform: translateY(2px);
}

View File

@@ -0,0 +1,38 @@
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import { FaCheck } from 'react-icons/fa6/index';
import styles from './checkbox.module.css';
type CheckboxInputProps = {
checked?: boolean;
className?: string;
defaultChecked?: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
};
export function Checkbox({
checked,
className,
defaultChecked = false,
disabled = false,
onChange,
}: CheckboxInputProps) {
const handleCheckedChange = (checked: boolean) => {
if (onChange) onChange(checked);
};
return (
<RadixCheckbox.Root
checked={checked}
className={`${styles.checkboxRoot} ${className}`}
defaultChecked={defaultChecked}
disabled={disabled}
onCheckedChange={handleCheckedChange}
>
<RadixCheckbox.Indicator className={styles.checkboxIndicator}>
<FaCheck />
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
);
}

View File

@@ -0,0 +1 @@
export { Checkbox } from './checkbox';

61
src/components/cipher.tsx Normal file
View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from 'react';
interface CipherTextProps {
interval?: number;
text: string;
}
const chars = '-_~`!@#$%^&*()+=[]{}|;:,.<>?';
export function CipherText({ interval = 50, text }: CipherTextProps) {
const [outputText, setOutputText] = useState('');
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setTimeout(() => setIsMounted(true), 2000);
}, []);
useEffect(() => {
if (!isMounted) return;
let timer: NodeJS.Timeout;
if (outputText !== text) {
timer = setInterval(() => {
if (outputText.length < text.length) {
setOutputText(prev => prev + text[prev.length]);
} else {
clearInterval(timer);
}
}, interval);
}
return () => clearInterval(timer);
}, [text, interval, outputText, isMounted]);
useEffect(() => {
if (outputText === text) {
setTimeout(() => setOutputText(''), 6000);
}
}, [outputText, text]);
const remainder =
outputText.length < text.length
? text
.slice(outputText.length)
.split('')
.map(() => chars[Math.floor(Math.random() * chars.length)])
.join('')
: '';
if (!isMounted) {
return <span>{text}</span>;
}
return (
<span className="text-white">
{outputText}
{remainder}
</span>
);
}

View File

@@ -38,7 +38,7 @@ import { Container } from './container';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-200),
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);

View File

@@ -11,24 +11,25 @@ const count = soundCount();
<div class="hero">
<Container>
<div class="wrapper">
<img
alt="Faded Moodist Logo"
class="logo"
height={45}
src="/logo.svg"
width={45}
/>
<div class="title">
<div class="left"></div>
<h2>Moodist</h2>
<div class="right"></div>
<div class="pattern"></div>
<div class="logo-wrapper">
<img
alt="Faded Moodist Logo"
aria-hidden="true"
class="logo"
height={48}
src="/logo.svg"
width={48}
/>
</div>
<h1 class="desc">Ambient sounds for focus and calm.</h1>
<h1 class="title">
Ambient Sounds<span class="line">For Focus and Calm</span>
</h1>
<h2 class="desc">Free and Open-Source.</h2>
<p class="sounds">
<span class="icon">
<span aria-hidden="true" class="icon">
<BsSoundwave />
</span>
<span>{count} Sounds</span>
@@ -43,54 +44,75 @@ const count = soundCount();
.wrapper {
position: relative;
padding: 100px 0 80px;
padding: 120px 0 80px;
& .pattern {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-image: radial-gradient(
var(--color-neutral-500) 5%,
transparent 5%
);
background-position: top center;
background-size: 21px 21px;
opacity: 0.8;
mask-image: linear-gradient(#fff, transparent, transparent);
&::before {
position: absolute;
top: 0;
left: 50%;
width: 300px;
height: 100px;
content: '';
background: var(--color-neutral-200);
filter: blur(50px);
border-radius: 100%;
opacity: 0.8;
transform: translate(-50%, -50%);
}
}
}
& .logo {
display: block;
width: 45px;
margin: 0 auto 12px;
& .logo-wrapper {
mask-image: linear-gradient(#000, rgb(0 0 0 / 40%), rgb(0 0 0 / 5%));
& .logo {
display: block;
width: 48px;
margin: 0 auto 20px;
opacity: 1;
animation-name: logo;
animation-duration: 60s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
}
& .title {
display: flex;
column-gap: 15px;
align-items: center;
font-family: var(--font-display);
font-size: var(--font-xlg);
font-weight: 600;
line-height: 1.1;
& div {
flex-grow: 1;
height: 1px;
&.left {
background: linear-gradient(
90deg,
transparent,
transparent,
var(--color-neutral-200),
var(--color-neutral-300)
);
}
&.right {
background: linear-gradient(
90deg,
var(--color-neutral-300),
var(--color-neutral-200),
transparent,
transparent
);
}
}
& h2 {
font-family: var(--font-display);
font-size: var(--font-2xlg);
font-weight: 600;
& .line {
display: block;
margin-top: 2px;
background: linear-gradient(
var(--color-foreground-subtler),
var(--color-foreground-subtle)
);
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
& .desc {
margin-top: 5px;
margin-top: 12px;
line-height: 1.6;
color: var(--color-foreground-subtle);
}
@@ -139,4 +161,14 @@ const count = soundCount();
}
}
}
@keyframes logo {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1 @@
export { MediaControls } from './media-controls';

View File

@@ -0,0 +1,20 @@
import { MediaSessionTrack } from './media-session-track';
import { useEffect, useState } from 'react';
import { useSSR } from '@/hooks/use-ssr';
export function MediaControls() {
const [mediaControlsEnabled, setMediaControlsEnabled] = useState(false);
const { isBrowser } = useSSR();
useEffect(() => {
if (!isBrowser) return;
setMediaControlsEnabled('mediaSession' in navigator);
}, [isBrowser]);
if (!mediaControlsEnabled) {
return null;
}
return <MediaSessionTrack />;
}

View File

@@ -0,0 +1,98 @@
import { useCallback, useEffect, useRef } from 'react';
import { BrowserDetect } from '@/helpers/browser-detect';
import { useSoundStore } from '@/stores/sound';
import { useSSR } from '@/hooks/use-ssr';
import { useDarkTheme } from '@/hooks/use-dark-theme';
const metadata: MediaMetadataInit = {
artist: 'Moodist',
title: 'Ambient Sounds for Focus and Calm',
};
export function MediaSessionTrack() {
const { isBrowser } = useSSR();
const isDarkTheme = useDarkTheme();
const isPlaying = useSoundStore(state => state.isPlaying);
const play = useSoundStore(state => state.play);
const pause = useSoundStore(state => state.pause);
const masterAudioSoundRef = useRef<HTMLAudioElement>(null);
const artworkURL = isDarkTheme ? '/logo-dark.png' : '/logo-light.png';
useEffect(() => {
if (!isBrowser || !isPlaying) return;
navigator.mediaSession.metadata = new MediaMetadata({
...metadata,
artwork: [
{
sizes: '200x200',
src: artworkURL,
type: 'image/png',
},
],
});
}, [artworkURL, isBrowser, isDarkTheme, isPlaying]);
const startMasterAudio = useCallback(async () => {
if (!masterAudioSoundRef.current) return;
if (!masterAudioSoundRef.current.paused) return;
try {
await masterAudioSoundRef.current.play();
navigator.mediaSession.playbackState = 'playing';
navigator.mediaSession.setActionHandler('play', play);
navigator.mediaSession.setActionHandler('pause', pause);
} catch {
// Do nothing
}
}, [pause, play]);
const stopMasterAudio = useCallback(() => {
if (!masterAudioSoundRef.current) return;
/**
* Otherwise in Safari we cannot play the audio again
* through the media session controls
*/
if (BrowserDetect.isSafari()) {
masterAudioSoundRef.current.load();
} else {
masterAudioSoundRef.current.pause();
}
navigator.mediaSession.playbackState = 'paused';
}, []);
useEffect(() => {
if (!masterAudioSoundRef.current) return;
if (isPlaying) {
startMasterAudio();
} else {
stopMasterAudio();
}
}, [isPlaying, startMasterAudio, stopMasterAudio]);
useEffect(() => {
const masterAudioSound = masterAudioSoundRef.current;
return () => {
masterAudioSound?.pause();
navigator.mediaSession.setActionHandler('play', null);
navigator.mediaSession.setActionHandler('pause', null);
navigator.mediaSession.playbackState = 'none';
};
}, []);
return (
<audio
id="media-session-track"
loop
ref={masterAudioSoundRef}
src="/sounds/silence.wav"
/>
);
}

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { AnimatePresence, motion } from 'motion/react';
import { IoClose } from 'react-icons/io5/index';
import FocusTrap from 'focus-trap-react';
@@ -19,6 +19,8 @@ interface ModalProps {
wide?: boolean;
}
const TRANSITION_DURATION = 300;
export function Modal({
children,
lockBody = true,
@@ -34,9 +36,12 @@ export function Modal({
useEffect(() => {
if (show && lockBody) {
document.body.style.overflow = 'hidden';
document.body.style.overflowY = 'hidden';
} else if (lockBody) {
document.body.style.overflow = 'auto';
// Wait for transition to finish before allowing scrollbar to return
setTimeout(() => {
document.body.style.overflowY = 'auto';
}, TRANSITION_DURATION);
}
}, [show, lockBody]);
@@ -68,6 +73,7 @@ export function Modal({
<motion.div
{...animationProps}
className={styles.overlay}
transition={{ duration: TRANSITION_DURATION / 1000 }}
variants={variants.overlay}
onClick={onClose}
onKeyDown={onClose}
@@ -76,6 +82,7 @@ export function Modal({
<motion.div
{...animationProps}
className={cn(styles.content, wide && styles.wide)}
transition={{ duration: TRANSITION_DURATION / 1000 }}
variants={variants.modal}
>
<button className={styles.close} onClick={onClose}>

View File

@@ -0,0 +1,76 @@
.header {
margin-bottom: 16px;
& .title {
margin-bottom: 4px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .desc {
color: var(--color-foreground-subtle);
}
}
.fieldWrapper {
margin-bottom: 12px;
& label {
display: block;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
& input,
select {
display: block;
width: 100%;
min-width: 0;
height: 45px;
padding: 0 8px;
margin-top: 4px;
color: var(--color-foreground);
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
outline: none;
}
& .volume {
margin-top: 4px;
}
}
}
.buttons {
display: flex;
column-gap: 8px;
align-items: center;
margin-top: 12px;
& button {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
height: 45px;
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-200);
border: none;
border-radius: 8px;
outline: none;
&:disabled {
cursor: not-allowed;
opacity: 0.3;
}
&.primary {
color: var(--color-neutral-50);
background-color: var(--color-neutral-950);
}
}
}

View File

@@ -0,0 +1,243 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider';
import styles from './binaural.module.css';
interface BinauralProps {
onClose: () => void;
show: boolean;
}
interface Preset {
baseFrequency: number;
beatFrequency: number;
name: string;
}
const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
];
function computeBinauralBeatOscillatorFrequencies(
baseFrequency: number,
beatFrequency: number,
) {
return {
leftFrequency: baseFrequency - beatFrequency / 2,
rightFrequency: baseFrequency + beatFrequency / 2,
};
}
export function BinauralModal({ onClose, show }: BinauralProps) {
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
const audioContextRef = useRef<AudioContext | null>(null);
const leftOscillatorRef = useRef<OscillatorNode | null>(null);
const rightOscillatorRef = useRef<OscillatorNode | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
const startSound = () => {
if (isPlaying) return;
// Initialize the AudioContext
audioContextRef.current = new window.AudioContext();
const audioContext = audioContextRef.current;
if (!audioContext) return;
// Create a gain node for volume control
gainNodeRef.current = audioContext.createGain();
gainNodeRef.current.gain.value = volume; // Set volume based on state
// Create oscillators for left and right channels
leftOscillatorRef.current = audioContext.createOscillator();
rightOscillatorRef.current = audioContext.createOscillator();
if (
!leftOscillatorRef.current ||
!rightOscillatorRef.current ||
!gainNodeRef.current
)
return;
const { leftFrequency, rightFrequency } =
computeBinauralBeatOscillatorFrequencies(baseFrequency, beatFrequency);
leftOscillatorRef.current.frequency.value = leftFrequency;
rightOscillatorRef.current.frequency.value = rightFrequency;
// Pan oscillators to left and right
const leftPanner = audioContext.createStereoPanner();
leftPanner.pan.value = -1;
const rightPanner = audioContext.createStereoPanner();
rightPanner.pan.value = 1;
// Connect nodes
leftOscillatorRef.current.connect(leftPanner).connect(gainNodeRef.current);
rightOscillatorRef.current
.connect(rightPanner)
.connect(gainNodeRef.current);
gainNodeRef.current.connect(audioContext.destination);
// Start oscillators
leftOscillatorRef.current.start();
rightOscillatorRef.current.start();
setIsPlaying(true);
};
const stopSound = useCallback(() => {
if (!isPlaying) return;
leftOscillatorRef.current?.stop();
rightOscillatorRef.current?.stop();
audioContextRef.current?.close();
setIsPlaying(false);
}, [isPlaying]);
useEffect(() => {
// Update gain node when volume changes
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = volume;
}
}, [volume]);
useEffect(() => {
// Update base frequency for both left and right oscillators when it changes
if (leftOscillatorRef.current && rightOscillatorRef.current) {
const { leftFrequency, rightFrequency } =
computeBinauralBeatOscillatorFrequencies(baseFrequency, beatFrequency);
leftOscillatorRef.current.frequency.value = leftFrequency;
rightOscillatorRef.current.frequency.value = rightFrequency;
}
}, [baseFrequency, beatFrequency]);
useEffect(() => {
// Cleanup when component unmounts
return () => {
if (isPlaying) {
stopSound();
}
};
}, [isPlaying, stopSound]);
useEffect(() => {
// Update frequencies when a preset is selected
if (selectedPreset !== 'Custom') {
const preset = presets.find(p => p.name === selectedPreset);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
}
}, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
setSelectedPreset(selected);
if (selected === 'Custom') {
// Allow user to input custom frequencies
return;
}
const preset = presets.find(p => p.name === selected);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
};
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Binaural Beat</h2>
<p className={styles.desc}>Binaural beat generator.</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
</option>
))}
</select>
</label>
</div>
{selectedPreset === 'Custom' && (
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
<input
max="1500"
min="20"
step="0.1"
type="number"
value={baseFrequency}
onChange={e =>
setBaseFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
<div className={styles.fieldWrapper}>
<label>
Beat Frequency (Hz):
<input
max="40"
min="0.1"
step="0.1"
type="number"
value={beatFrequency}
onChange={e =>
setBeatFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
</>
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
<Slider
className={styles.volume}
max={1}
min={0}
step={0.01}
value={volume}
onChange={value => setVolume(value)}
/>
</label>
</div>
<div className={styles.buttons}>
<button
className={styles.primary}
disabled={isPlaying}
onClick={startSound}
>
Start
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
</button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1 @@
export { BinauralModal } from './binaural';

View File

@@ -8,7 +8,7 @@ interface TimerProps {
show: boolean;
}
export function BreathingExercise({ onClose, show }: TimerProps) {
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Breathing Exercise</h2>

View File

@@ -0,0 +1,91 @@
.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;
& .timer {
position: absolute;
top: 4px;
left: 4px;
padding: 4px 12px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(
var(--color-neutral-100),
var(--color-neutral-50)
);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
}
& .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%);
}
}
.selectWrapper {
position: relative;
width: 100%;
height: 45px;
padding: 0 12px;
margin-top: 8px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 80%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
& .selectBox {
width: 100%;
min-width: 0;
height: 100%;
font-size: var(--font-sm);
color: var(--color-foreground);
background-color: transparent;
border: none;
outline: none;
& option {
color: var(--color-neutral-50);
}
}
}

View File

@@ -0,0 +1,126 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { motion } from 'motion/react';
import { padNumber } from '@/helpers/number';
import styles from './exercise.module.css';
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
'4-7-8 Breathing': ['inhale', 'holdInhale', 'exhale'],
'Box Breathing': ['inhale', 'holdInhale', 'exhale', 'holdExhale'],
'Resonant Breathing': ['inhale', 'exhale'],
};
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 },
'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale
};
const PHASE_LABELS: Record<Phase, string> = {
exhale: 'Exhale',
holdExhale: 'Hold',
holdInhale: 'Hold',
inhale: 'Inhale',
};
export function Exercise() {
const [selectedExercise, setSelectedExercise] =
useState<Exercise>('4-7-8 Breathing');
const [phaseIndex, setPhaseIndex] = useState(0);
const phases = useMemo(
() => EXERCISE_PHASES[selectedExercise],
[selectedExercise],
);
const durations = useMemo(
() => EXERCISE_DURATIONS[selectedExercise],
[selectedExercise],
);
const currentPhase = phases[phaseIndex];
const animationVariants = useMemo(
() => ({
exhale: {
transform: 'translate(-50%, -50%) scale(1)',
transition: { duration: durations.exhale },
},
holdExhale: {
transform: 'translate(-50%, -50%) scale(1)',
transition: { duration: durations.holdExhale },
},
holdInhale: {
transform: 'translate(-50%, -50%) scale(1.5)',
transition: { duration: durations.holdInhale },
},
inhale: {
transform: 'translate(-50%, -50%) scale(1.5)',
transition: { duration: durations.inhale },
},
}),
[durations],
);
const resetExercise = useCallback(() => {
setPhaseIndex(0);
}, []);
const updatePhase = useCallback(() => {
setPhaseIndex(prevIndex => (prevIndex + 1) % phases.length);
}, [phases.length]);
useEffect(() => {
resetExercise();
}, [selectedExercise, resetExercise]);
useEffect(() => {
const intervalDuration = (durations[currentPhase] || 4) * 1000;
const interval = setInterval(updatePhase, intervalDuration);
return () => clearInterval(interval);
}, [currentPhase, durations, updatePhase]);
const [timer, setTimer] = useState(0);
useEffect(() => {
const interval = setInterval(() => setTimer(prev => prev + 1), 1000);
return () => clearInterval(interval);
}, []);
return (
<>
<div className={styles.exercise}>
<div className={styles.timer}>
{padNumber(Math.floor(timer / 60))}:{padNumber(timer % 60)}
</div>
<motion.div
animate={currentPhase}
className={styles.circle}
key={selectedExercise}
variants={animationVariants}
/>
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
</div>
<div className={styles.selectWrapper}>
<select
className={styles.selectBox}
value={selectedExercise}
onChange={e => setSelectedExercise(e.target.value as Exercise)}
>
{Object.keys(EXERCISE_PHASES).map(exercise => (
<option key={exercise} value={exercise}>
{exercise}
</option>
))}
</select>
</div>
</>
);
}

View File

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

View File

@@ -0,0 +1 @@
export { IsochronicModal } from './isochronic';

View File

@@ -0,0 +1,76 @@
.header {
margin-bottom: 16px;
& .title {
margin-bottom: 4px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .desc {
color: var(--color-foreground-subtle);
}
}
.fieldWrapper {
margin-bottom: 12px;
& label {
display: block;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
& input,
select {
display: block;
width: 100%;
min-width: 0;
height: 45px;
padding: 0 8px;
margin-top: 4px;
color: var(--color-foreground);
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
outline: none;
}
& .volume {
margin-top: 4px;
}
}
}
.buttons {
display: flex;
column-gap: 8px;
align-items: center;
margin-top: 12px;
& button {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
height: 45px;
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-200);
border: none;
border-radius: 8px;
outline: none;
&:disabled {
cursor: not-allowed;
opacity: 0.3;
}
&.primary {
color: var(--color-neutral-50);
background-color: var(--color-neutral-950);
}
}
}

View File

@@ -0,0 +1,258 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider';
import styles from './isochornic.module.css';
interface IsochronicProps {
onClose: () => void;
show: boolean;
}
interface Preset {
baseFrequency: number;
beatFrequency: number;
name: string;
}
const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
];
export function IsochronicModal({ onClose, show }: IsochronicProps) {
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
const [waveform] = useState<OscillatorType>('sine'); // Default waveform
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
const audioContextRef = useRef<AudioContext | null>(null);
const oscillatorRef = useRef<OscillatorNode | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
const beatGainRef = useRef<GainNode | null>(null);
const modulatorRef = useRef<OscillatorNode | null>(null);
const startSound = () => {
if (isPlaying) return;
audioContextRef.current = new window.AudioContext();
const audioContext = audioContextRef.current;
if (!audioContext) return;
// Main gain node for volume control
gainNodeRef.current = audioContext.createGain();
gainNodeRef.current.gain.value = volume;
// Oscillator for the base tone
oscillatorRef.current = audioContext.createOscillator();
oscillatorRef.current.frequency.value = baseFrequency;
oscillatorRef.current.type = waveform;
// Gain node to create isochronic beats
beatGainRef.current = audioContext.createGain();
beatGainRef.current.gain.value = 0; // Start with silence
// Oscillator for modulation
modulatorRef.current = audioContext.createOscillator();
modulatorRef.current.frequency.value = beatFrequency;
modulatorRef.current.type = 'square'; // Square wave for on/off effect
// Modulator gain to adjust modulation depth
const modulatorGain = audioContext.createGain();
modulatorGain.gain.value = 0.5; // Modulation depth
// Connect modulator to the beat gain node
modulatorRef.current
.connect(modulatorGain)
.connect(beatGainRef.current.gain);
// Connect oscillator through beat gain and main gain to destination
oscillatorRef.current
.connect(beatGainRef.current)
.connect(gainNodeRef.current)
.connect(audioContext.destination);
// Start oscillators
oscillatorRef.current.start();
modulatorRef.current.start();
setIsPlaying(true);
};
const stopSound = useCallback(() => {
if (!isPlaying) return;
oscillatorRef.current?.stop();
modulatorRef.current?.stop();
audioContextRef.current?.close();
setIsPlaying(false);
}, [isPlaying]);
useEffect(() => {
// Update gain when volume changes
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = volume;
}
}, [volume]);
useEffect(() => {
// Update base frequency when it changes
if (oscillatorRef.current) {
oscillatorRef.current.frequency.value = baseFrequency;
}
}, [baseFrequency]);
useEffect(() => {
// Update beat frequency when it changes
if (modulatorRef.current) {
modulatorRef.current.frequency.value = beatFrequency;
}
}, [beatFrequency]);
useEffect(() => {
// Update waveform when it changes
if (oscillatorRef.current) {
oscillatorRef.current.type = waveform;
}
}, [waveform]);
useEffect(() => {
// Cleanup when component unmounts
return () => {
if (isPlaying) {
stopSound();
}
};
}, [isPlaying, stopSound]);
useEffect(() => {
// Update frequencies when a preset is selected
if (selectedPreset !== 'Custom') {
const preset = presets.find(p => p.name === selectedPreset);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
}
}, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
setSelectedPreset(selected);
if (selected === 'Custom') {
// Allow user to input custom frequencies
return;
}
const preset = presets.find(p => p.name === selected);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
};
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Isochronic Tone</h2>
<p className={styles.desc}>Isochronic tone generator.</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
</option>
))}
</select>
</label>
</div>
{selectedPreset === 'Custom' && (
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
<input
max="2000"
min="20"
step="0.1"
type="number"
value={baseFrequency}
onChange={e =>
setBaseFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
<div className={styles.fieldWrapper}>
<label>
Tone Frequency (Hz):
<input
max="40"
min="0.1"
step="0.1"
type="number"
value={beatFrequency}
onChange={e =>
setBeatFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
{/* <div className={styles.fieldWrapper}>
<label>
Waveform:
<select
value={waveform}
onChange={e => setWaveform(e.target.value as OscillatorType)}
>
<option value="sine">Sine</option>
<option value="square">Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle">Triangle</option>
</select>
</label>
</div> */}
</>
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
<Slider
className={styles.volume}
max={1}
min={0}
step={0.01}
value={volume}
onChange={value => setVolume(value)}
/>
</label>
</div>
<div className={styles.buttons}>
<button
className={styles.primary}
disabled={isPlaying}
onClick={startSound}
>
Start
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
</button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1 @@
export { LofiModal } from './lofi';

View File

@@ -0,0 +1,86 @@
.title {
margin-bottom: 12px;
font-family: var(--font-heading);
font-weight: 600;
}
.notice {
& p {
line-height: 1.4;
color: var(--color-foreground-subtle);
}
& .buttons {
display: flex;
column-gap: 8px;
align-items: center;
justify-content: flex-end;
margin-top: 12px;
& button {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
padding: 0 16px;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background: var(--color-neutral-200);
border: none;
border-radius: 8px;
&.primary {
color: var(--color-neutral-50);
background: var(--color-neutral-950);
}
}
}
}
.videos {
margin-top: 20px;
& .video {
&:not(:last-of-type) {
margin-bottom: 24px;
}
& h2 {
margin-bottom: 8px;
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
& span {
display: inline-block;
color: var(--color-foreground-subtler);
&.index {
margin-right: 4px;
}
}
& strong {
font-weight: 500;
color: var(--color-foreground);
}
}
& .container {
padding: 8px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-300);
border-radius: 12px;
& .iframe {
width: 100%;
max-width: 100%;
height: auto;
aspect-ratio: 560 / 315;
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
}
}
}
}

View File

@@ -0,0 +1,85 @@
import { useState } from 'react';
import YouTube from 'react-youtube';
import { Modal } from '@/components/modal/modal';
import styles from './lofi.module.css';
import { padNumber } from '@/helpers/number';
interface LofiProps {
onClose: () => void;
show: boolean;
}
const videos = [
{
channel: 'Lofi Girl',
id: 'jfKfPfyJRdk',
title: 'lofi hip hop radio',
},
{
channel: 'Lofi Girl',
id: '4xDzrJKXOOY',
title: 'synthwave radio',
},
{
channel: 'Lofi Girl',
id: 'P6Segk8cr-c',
title: 'sad lofi radio',
},
{
channel: 'Lofi Girl',
id: 'S_MOd40zlYU',
title: 'dark ambient radio',
},
{
channel: 'Lofi Girl',
id: 'TtkFsfOP9QI',
title: 'peaceful piano radio',
},
];
export function LofiModal({ onClose, show }: LofiProps) {
const [isAccepted, setIsAccepted] = useState(false);
return (
<Modal persist show={show} onClose={onClose}>
<h1 className={styles.title}>Lofi Music Player</h1>
{!isAccepted ? (
<div className={styles.notice}>
<p>
This feature plays music using embedded YouTube videos. By
continuing, you agree to connect to YouTube, which may collect data
in accordance with their privacy policy. We do not control or track
this data.
</p>
<div className={styles.buttons}>
<button onClick={onClose}>Cancel</button>
<button
className={styles.primary}
onClick={() => setIsAccepted(true)}
>
Continue
</button>
</div>
</div>
) : (
<div className={styles.videos}>
{videos.map((video, index) => (
<div className={styles.video} key={video.id}>
<h2>
<span className={styles.index}>{padNumber(index + 1, 2)}</span>{' '}
<strong>{video.channel}</strong> <span>/</span> {video.title}
</h2>
<div className={styles.container}>
<YouTube iframeClassName={styles.iframe} videoId={video.id} />
</div>
</div>
))}
</div>
)}
</Modal>
);
}

View File

@@ -18,7 +18,7 @@
justify-content: space-between;
& .label {
font-weight: 500;
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
}

View File

@@ -22,24 +22,28 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
label: 'Share Sounds',
},
{
keys: ['Shift', 'N'],
label: 'Notepad',
},
{
keys: ['Shift', 'P'],
label: 'Pomodoro Timer',
},
{
keys: ['Shift', 'B'],
label: 'Breathing Exercise',
keys: ['Shift', 'Alt', 'T'],
label: 'Sleep Timer',
},
{
keys: ['Shift', 'C'],
label: 'Countdown Timer',
},
{
keys: ['Shift', 'P'],
label: 'Pomodoro',
},
{
keys: ['Shift', 'N'],
label: 'Notepad',
},
{
keys: ['Shift', 'T'],
label: 'Sleep Timer',
label: 'Todo Checklist',
},
{
keys: ['Shift', 'B'],
label: 'Breathing Exercise',
},
{
keys: ['Shift', 'Space'],

View File

@@ -1,5 +1,5 @@
.header {
margin-bottom: 16px;
margin-bottom: 28px;
& .title {
margin-bottom: 8px;
@@ -28,6 +28,7 @@
& .label {
display: block;
margin-bottom: 4px;
font-size: var(--font-sm);
font-weight: 500;
}

View File

@@ -1,7 +1,7 @@
import { useEffect, useState, useRef, useMemo } from 'react';
import { Modal } from '@/components/modal';
import { Timer } from '@/components/timer';
import { Timer } from './timer';
import { dispatch } from '@/lib/event';
import { useSoundStore } from '@/stores/sound';
import { cn } from '@/helpers/styles';
@@ -17,6 +17,7 @@ interface SleepTimerModalProps {
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
const setActive = useSleepTimerStore(state => state.set);
const noSelected = useSoundStore(state => state.noSelected());
const [running, setRunning] = useState(false);
@@ -47,6 +48,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
const handleStart = () => {
if (timerId.current) clearInterval(timerId.current);
if (noSelected) return;
if (!isPlaying) play();
if (totalSeconds > 0) {
@@ -63,7 +65,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
useEffect(() => {
if (timeLeft === 0) {
setRunning(false);
// pause();
dispatch(FADE_OUT, { duration: 1000 });
setTimeSpent(0);
@@ -107,7 +109,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
)}
</div>
{running ? <Timer displayHours={true} timer={timeLeft} /> : null}
{running ? <Timer reverse={timeSpent} timer={timeLeft} /> : null}
<div className={styles.buttons}>
{running && (

View File

@@ -0,0 +1 @@
export { Reverse } from './reverse';

View File

@@ -0,0 +1,30 @@
.reverse {
position: absolute;
top: 8px;
left: 8px;
padding: 4px 8px;
font-size: var(--font-2xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
&::after {
position: absolute;
bottom: -1px;
left: 50%;
width: 75%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
}

View File

@@ -0,0 +1,27 @@
import { padNumber } from '@/helpers/number';
import styles from './reverse.module.css';
interface ReverseProps {
time: number;
}
export function Reverse({ time }: ReverseProps) {
let hours = Math.floor(time / 3600);
let minutes = Math.floor((time % 3600) / 60);
let seconds = time % 60;
hours = isNaN(hours) ? 0 : hours;
minutes = isNaN(minutes) ? 0 : minutes;
seconds = isNaN(seconds) ? 0 : seconds;
const formattedHours = padNumber(hours);
const formattedMinutes = padNumber(minutes);
const formattedSeconds = padNumber(seconds);
return (
<div className={styles.reverse}>
- {formattedHours}:{formattedMinutes}:{formattedSeconds}
</div>
);
}

View File

@@ -0,0 +1,29 @@
.timer {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 48px 0;
font-size: var(--font-xlg);
font-weight: 500;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 12px;
&::after {
position: absolute;
bottom: -1px;
left: 50%;
width: 75%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);
}
}

View File

@@ -1,13 +1,15 @@
import { Reverse } from './reverse';
import { padNumber } from '@/helpers/number';
import styles from './timer.module.css';
interface TimerProps {
displayHours?: boolean;
reverse: number;
timer: number;
}
export function Timer({ displayHours = false, timer }: TimerProps) {
export function Timer({ reverse, timer }: TimerProps) {
let hours = Math.floor(timer / 3600);
let minutes = Math.floor((timer % 3600) / 60);
let seconds = timer % 60;
@@ -22,15 +24,8 @@ export function Timer({ displayHours = false, timer }: TimerProps) {
return (
<div className={styles.timer}>
{displayHours ? (
<>
{formattedHours}:{formattedMinutes}:{formattedSeconds}
</>
) : (
<>
{formattedMinutes}:{formattedSeconds}
</>
)}
<Reverse time={reverse} />
{formattedHours}:{formattedMinutes}:{formattedSeconds}
</div>
);
}

View File

@@ -0,0 +1 @@
export { Reload } from './reload';

View File

@@ -0,0 +1,36 @@
import { useRegisterSW } from 'virtual:pwa-register/react'; // eslint-disable-line
import { Modal } from '@/components/modal';
import styles from './reload.module.css';
export function ReloadModal() {
const {
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW();
const close = () => {
setNeedRefresh(false);
};
return (
<Modal show={needRefresh} onClose={close}>
<h2 className={styles.title}>New Content</h2>
<p className={styles.desc}>
New content available, click on reload button to update.
</p>
<div className={styles.buttons}>
<button onClick={close}>Close</button>
<button
className={styles.primary}
onClick={() => updateServiceWorker(true)}
>
Reload
</button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,38 @@
.title {
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
.desc {
margin-top: 8px;
color: var(--color-foreground-subtle);
}
.buttons {
display: flex;
column-gap: 8px;
align-items: center;
justify-content: flex-end;
margin-top: 20px;
& button {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
padding: 0 12px;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground-subtle);
cursor: pointer;
background-color: var(--color-neutral-200);
border: none;
border-radius: 8px;
&.primary {
color: var(--color-neutral-50);
background-color: var(--color-neutral-950);
}
}
}

View File

@@ -0,0 +1,11 @@
import { useEffect, useState } from 'react';
import { ReloadModal } from './reload-modal';
export function Reload() {
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => setIsBrowser(true), []);
return isBrowser ? <ReloadModal /> : null;
}

View File

@@ -9,14 +9,16 @@ export function Shuffle() {
const shuffle = useSoundStore(state => state.shuffle);
return (
<Tooltip content="Shuffle sounds" showDelay={0}>
<button
aria-label="Shuffle sounds"
className={styles.button}
onClick={shuffle}
>
<BiShuffle />
</button>
</Tooltip>
<Tooltip.Provider delayDuration={0}>
<Tooltip content="Shuffle sounds">
<button
aria-label="Shuffle sounds"
className={styles.button}
onClick={shuffle}
>
<BiShuffle />
</button>
</Tooltip>
</Tooltip.Provider>
);
}

View File

@@ -0,0 +1 @@
export { Slider } from './slider';

View File

@@ -0,0 +1,42 @@
.sliderRoot {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 20px;
touch-action: none;
}
.sliderTrack {
position: relative;
flex-grow: 1;
height: 4px;
background: var(--color-neutral-200);
border-radius: 9999px;
}
.sliderRange {
position: absolute;
height: 100%;
background: var(--color-neutral-800);
border-radius: 9999px;
}
.sliderThumb {
display: block;
width: 16px;
height: 16px;
cursor: pointer;
background: var(--color-neutral-950);
border-radius: 50%;
box-shadow: 0 0 3px var(--color-neutral-50);
}
.sliderThumb:hover {
background: var(--color-neutral-800);
}
.sliderThumb:focus {
outline: none;
box-shadow: 0 0 0 3px var(--color-neutral-400);
}

View File

@@ -0,0 +1,47 @@
import * as RadixSlider from '@radix-ui/react-slider';
import styles from './slider.module.css';
type SliderProps = {
className?: string;
defaultValue?: number;
disabled?: boolean;
max?: number;
min?: number;
onChange?: (value: number) => void;
step?: number;
value?: number;
};
export function Slider({
className,
defaultValue = 50,
disabled = false,
max = 100,
min = 0,
onChange,
step = 1,
value,
}: SliderProps) {
const handleValueChange = (values: number[]) => {
if (onChange) onChange(values[0]);
};
return (
<RadixSlider.Root
className={`${styles.sliderRoot} ${className}`}
defaultValue={[defaultValue]}
disabled={disabled}
max={max}
min={min}
step={step}
tabIndex={0}
value={value !== undefined ? [value] : undefined}
onValueChange={handleValueChange}
>
<RadixSlider.Track className={styles.sliderTrack}>
<RadixSlider.Range className={styles.sliderRange} />
</RadixSlider.Track>
<RadixSlider.Thumb className={styles.sliderThumb} />
</RadixSlider.Root>
);
}

View File

@@ -13,8 +13,8 @@
margin: 0 auto;
font-size: var(--font-sm);
pointer-events: fill;
background-color: var(--color-neutral-200);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 4px;
border-radius: 8px;
}
}

View File

@@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { motion } from 'motion/react';
import { mix, fade, slideY } from '@/lib/motion';

View File

@@ -1,5 +1,5 @@
import { BiHeart, BiSolidHeart } from 'react-icons/bi/index';
import { AnimatePresence, motion } from 'framer-motion';
import { AnimatePresence, motion } from 'motion/react';
import { useSoundStore } from '@/stores/sound';
import { cn } from '@/helpers/styles';
@@ -56,6 +56,7 @@ export function Favorite({ id, label }: FavoriteProps) {
>
<motion.span
animate="show"
aria-hidden="true"
exit="hidden"
initial="hidden"
key={isFavorite ? `${id}-is-favorite` : `${id}-not-favorite`}

Some files were not shown because too many files have changed in this diff Show More