88 Commits

Author SHA1 Message Date
MAZE
4b5456a51d chore(release): 1.4.3 2024-04-11 16:19:31 +03:30
MAZE
fa9711a1e0 chore: remove arm/v7 2024-04-11 16:19:04 +03:30
MAZE
73a8e03d66 chore(release): 1.4.2 2024-04-11 15:38:00 +03:30
MAZE
017c27fb2b chore: remove arm/v6 2024-04-11 15:37:39 +03:30
MAZE
43ba975408 style: change font path 2024-04-11 15:37:11 +03:30
MAZE
42bd47bbea chore(release): 1.4.1 2024-04-11 15:14:42 +03:30
MAZE
faf7f78b8c chore: change GitHub workflow 2024-04-11 15:13:49 +03:30
MAZE
7ec7ea74d5 style: widen the container 2024-04-11 15:12:56 +03:30
MAZE
ede480186c feat: add toolbar and portal 2024-04-08 20:55:03 +03:30
MAZE
6dfa998ffe chore(release): 1.4.0 2024-02-29 15:55:04 +03:30
MAZE
a8a8c36434 fix: comment out toolbox section 2024-02-29 15:53:38 +03:30
MAZE
7390a9b3de fix: remove extra headings 2024-02-29 15:52:48 +03:30
MAZE
0eb47ba2e1 feat: add alarm for pomodoro timer 2024-02-29 15:19:57 +03:30
MAZE
110356b2da fix: remove time from tabs array 2024-02-29 15:06:37 +03:30
MAZE
158cffca8c fix: change default times 2024-02-29 15:05:54 +03:30
MAZE
def69de6e4 style: add scroll lock in modals 2024-02-28 23:03:35 +03:30
MAZE
69cb45bff7 style: decrease font size 2024-02-28 22:56:08 +03:30
MAZE
e7fd84bd4e style: change position for toolbar 2024-02-28 22:54:54 +03:30
MAZE
dfd6c1fc4a fix: add min-width to inputs 2024-02-28 20:27:45 +03:30
MAZE
38a9a23790 feat: persist presets 2024-02-28 20:19:36 +03:30
MAZE
2484e01273 feat: add custom presets 2024-02-28 19:59:39 +03:30
MAZE
98d2f76438 style: decrease padding 2024-02-27 20:40:54 +03:30
MAZE
0e12a5203e feat: add CTA button 2024-02-27 20:27:15 +03:30
MAZE
3205145d54 feat: add copy for productivity toolbox 2024-02-27 20:15:51 +03:30
MAZE
8f36c863d7 style: remove hero pattern 2024-02-27 20:14:30 +03:30
MAZE
941e1f0241 feat: change tooltip content 2024-02-25 21:51:31 +03:30
MAZE
240fd9c6e0 feat: add active indicators 2024-02-25 21:42:40 +03:30
MAZE
665e2173f4 feat: persist pomodoro setting 2024-02-25 21:29:52 +03:30
MAZE
1ac52861d1 fix: change completion condition 2024-02-25 21:22:35 +03:30
MAZE
a7e5368591 fix: change initial value 2024-02-25 17:21:18 +03:30
MAZE
f3cb2a1b63 feat: implement time setting 2024-02-25 17:19:06 +03:30
MAZE
586e502c3c style: change unselected style 2024-02-25 15:12:26 +03:30
MAZE
5b83710c75 style: increase pattern opacity 2024-02-25 15:08:56 +03:30
MAZE
7ed016d855 feat: add controls to pomodoro 2024-02-25 15:04:25 +03:30
MAZE
9f7de336e5 feat: add basic pomodoro structure 2024-02-25 13:47:38 +03:30
MAZE
758f2f48dc feat: add scroll for lower heights 2024-02-24 20:22:36 +03:30
MAZE
d055e66dd9 feat: add source code item 2024-02-24 19:54:41 +03:30
MAZE
79afb8d92f fix: change z-index values 2024-02-24 19:35:10 +03:30
MAZE
408734d49f feat: add dividers to menu items 2024-02-24 19:33:13 +03:30
MAZE
7463334053 feat: add toolbar to notepad 2024-02-24 19:23:17 +03:30
MAZE
ae3ea8c74f fix: remove word counter dependency 2024-02-24 16:33:41 +03:30
MAZE
24245235b1 feat: add counter to notepad 2024-02-24 16:25:34 +03:30
MAZE
b143e46e92 Merge branch 'main' into develop 2024-02-24 16:13:33 +03:30
MAZE
e923559709 feat: add simple notepad 2024-02-24 16:06:53 +03:30
MAZE
8930e7b76a style: change description 2024-02-17 18:37:25 +03:30
MAZE
5f40435c0c feat: add titles 2024-02-16 16:46:47 +03:30
MAZE
38c11f124e fix: add key to categories 2024-02-11 19:52:10 +03:30
MAZE
a5d2ba45f8 Merge branch 'main' into develop 2024-02-11 19:34:03 +03:30
MAZE
653d309e64 fix: remove fading 2024-02-11 19:33:56 +03:30
MAZE
663cb92135 feat: add fade in/out 2024-02-11 19:25:25 +03:30
MAZE
1a499be244 style: add effect to about 2024-02-09 20:56:33 +03:30
MAZE
4515aa8e7a style: change the about style 2024-02-09 20:42:42 +03:30
MAZE
831a9c8ea0 refactor: add variant to container 2024-02-09 18:29:00 +03:30
MAZE
f66a6ffde7 fix: make sound count dynamic 2024-02-09 17:29:05 +03:30
MAZE
9028675057 style: hide features 2024-02-09 17:26:45 +03:30
MAZE
37505a6b3f style: show about and features 2024-02-09 15:20:14 +03:30
MAZE
400ea0aeaf style: hide about and features 2024-02-09 14:45:15 +03:30
MAZE
e4e332ad75 feat: add features section 2024-02-09 14:35:49 +03:30
MAZE
341a896924 style: revert changes 2024-02-09 11:54:09 +03:30
MAZE
1a6ecd82ab style: decorate paragraphs 2024-02-09 00:44:28 +03:30
MAZE
d725d59703 feat: add about section 2024-02-08 22:44:22 +03:30
MAZE
52176bc3f9 refactor: move footer to React 2024-02-08 19:49:35 +03:30
MAZE
c505c574a8 refactor: move donation to React 2024-02-08 19:47:31 +03:30
MAZE
3f45be3942 refactor: remove sections 2024-02-08 19:44:30 +03:30
MAZE
a514a364ec feat: add special button 2024-02-08 19:43:50 +03:30
MAZE
81e6666776 fix: add correct count to description 2024-02-08 19:35:44 +03:30
MAZE
56802b67f2 style: decrease opacity 2024-02-08 19:31:20 +03:30
MAZE
d4cc24e468 style: lower opacity 2024-02-08 19:23:29 +03:30
MAZE
f7302dec5b feat: add open-source section 2024-02-08 19:17:14 +03:30
MAZE
1a01a00866 feat: change alignments 2024-02-08 18:59:58 +03:30
MAZE
38da02a0d3 feat: change the copy for features 2024-02-08 18:32:08 +03:30
MAZE
5fc3e7e5d0 style: change the pattern slightly 2024-02-08 18:15:47 +03:30
MAZE
0f32de3c0c style: decrease shine 2024-02-07 21:51:17 +03:30
MAZE
ac24da2940 style: add shine to donation button 2024-02-07 21:46:26 +03:30
MAZE
5916e86d3c style: revert pattern 2024-02-07 21:38:28 +03:30
MAZE
f3e7224267 style: change pattern 2024-02-07 21:31:35 +03:30
MAZE
e1b9a1736c style: change sound counter 2024-02-07 18:50:57 +03:30
MAZE
2078648c66 style: decrease opacity 2024-02-07 18:42:34 +03:30
MAZE
6d30a0123e style: add margin to donate section 2024-02-07 16:23:46 +03:30
MAZE
8471a3ca49 style: change dividers 2024-02-07 16:22:17 +03:30
MAZE
182a8c7aad style: decrease dots 2024-02-07 15:56:15 +03:30
MAZE
0ad4bb72e1 style: decrease dots 2024-02-07 15:37:07 +03:30
MAZE
405fcccd95 style: increase dots 2024-02-07 15:30:28 +03:30
MAZE
2b843747b4 style: decrease dots 2024-02-07 15:25:02 +03:30
MAZE
8e500136ce style: increase padding 2024-02-07 15:16:25 +03:30
MAZE
882d44079c style: increase opacity 2024-02-07 15:10:59 +03:30
MAZE
82e4ea72f4 style: decrease opacity 2024-02-07 15:10:20 +03:30
MAZE
dc22b51548 style: add polka dot pattern 2024-02-07 15:08:08 +03:30
109 changed files with 2480 additions and 723 deletions

View File

@@ -20,15 +20,21 @@ jobs:
username: ${{github.actor}}
password: ${{secrets.ACCESS_TOKEN}}
- name: 'Build Inventory Image'
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: 'Build and push Inventory Image'
run: |
IMAGE_NAME="ghcr.io/remvze/moodist"
GIT_TAG=${{ github.ref }}
GIT_TAG=${GIT_TAG#refs/tags/}
docker build . --tag $IMAGE_NAME:latest
docker push $IMAGE_NAME:latest
docker build . --tag $IMAGE_NAME:$GIT_TAG
docker push $IMAGE_NAME:$GIT_TAG
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t $IMAGE_NAME:latest \
-t $IMAGE_NAME:$GIT_TAG \
--push .

View File

@@ -2,6 +2,136 @@
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.
### [1.4.3](https://github.com/remvze/moodist/compare/v1.4.2...v1.4.3) (2024-04-11)
### 🚚 Chores
* remove arm/v7 ([fa9711a](https://github.com/remvze/moodist/commit/fa9711a1e09e6e979b420556160c3cd69a8c3775))
### [1.4.2](https://github.com/remvze/moodist/compare/v1.4.1...v1.4.2) (2024-04-11)
### 💄 Styling
* change font path ([43ba975](https://github.com/remvze/moodist/commit/43ba9754085d7a710d3d629e70f873b16f267507))
### 🚚 Chores
* remove arm/v6 ([017c27f](https://github.com/remvze/moodist/commit/017c27fb2b20402553011db8f417717dcca6d447))
### [1.4.1](https://github.com/remvze/moodist/compare/v1.4.0...v1.4.1) (2024-04-11)
### ✨ Features
* add toolbar and portal ([ede4801](https://github.com/remvze/moodist/commit/ede480186c4b3f187c82e1d64e4d521ee59da31a))
### 💄 Styling
* widen the container ([7ec7ea7](https://github.com/remvze/moodist/commit/7ec7ea74d53db85cffa3af646c03270793453009))
### 🚚 Chores
* change GitHub workflow ([faf7f78](https://github.com/remvze/moodist/commit/faf7f78b8c10cd7b3688ed5bba3d0c077c020ad2))
## [1.4.0](https://github.com/remvze/moodist/compare/v1.3.1...v1.4.0) (2024-02-29)
### ♻️ Code Refactoring
* add variant to container ([831a9c8](https://github.com/remvze/moodist/commit/831a9c8ea019a3d86e994ff0e060dd4337a84d1f))
* move donation to React ([c505c57](https://github.com/remvze/moodist/commit/c505c574a885004e071da63d8255062befc29921))
* move footer to React ([52176bc](https://github.com/remvze/moodist/commit/52176bc3f9eac43d701de0e7f0ca103eeca46858))
* remove sections ([3f45be3](https://github.com/remvze/moodist/commit/3f45be3942bfeff74f3f0126de5e61037a749e61))
### 💄 Styling
* add effect to about ([1a499be](https://github.com/remvze/moodist/commit/1a499be2446730d5333dd0d0d6a06bbd47564979))
* add margin to donate section ([6d30a01](https://github.com/remvze/moodist/commit/6d30a0123e0feb509b6c560f405b98d20a89467a))
* add polka dot pattern ([dc22b51](https://github.com/remvze/moodist/commit/dc22b51548f0d6830fcee79eebd75650f3f19dc1))
* add scroll lock in modals ([def69de](https://github.com/remvze/moodist/commit/def69de6e4e11e373280c90f93af0b0369b85ac8))
* add shine to donation button ([ac24da2](https://github.com/remvze/moodist/commit/ac24da294008a34c49dc3502374f1fcf55db5be8))
* change description ([8930e7b](https://github.com/remvze/moodist/commit/8930e7b76abffafd0ace5926de6c1d3f7629febd))
* change dividers ([8471a3c](https://github.com/remvze/moodist/commit/8471a3ca493b0c738ed7de900e82403f0b1ce2b7))
* change pattern ([f3e7224](https://github.com/remvze/moodist/commit/f3e72242670317d938cc8d9619729be95df0f4f0))
* change position for toolbar ([e7fd84b](https://github.com/remvze/moodist/commit/e7fd84bd4e8637e34eb0a59e97fd9c49875f8776))
* change sound counter ([e1b9a17](https://github.com/remvze/moodist/commit/e1b9a1736c1d11827faf900838769194364afbd3))
* change the about style ([4515aa8](https://github.com/remvze/moodist/commit/4515aa8e7a7f6d0fbc839625f74f0583e1a20d18))
* change the pattern slightly ([5fc3e7e](https://github.com/remvze/moodist/commit/5fc3e7e5d048cb4aa189683d147b181fdf2a94b6))
* change unselected style ([586e502](https://github.com/remvze/moodist/commit/586e502c3cc81314bc1e83f4e088a0d9289390fc))
* decorate paragraphs ([1a6ecd8](https://github.com/remvze/moodist/commit/1a6ecd82abe89e1686538c42b31826ccc8a43b2d))
* decrease dots ([182a8c7](https://github.com/remvze/moodist/commit/182a8c7aadc9a253261c56ae7faf8ac5c3c82707))
* decrease dots ([0ad4bb7](https://github.com/remvze/moodist/commit/0ad4bb72e15e8f7d52e7d4b036b71fdb837e3554))
* decrease dots ([2b84374](https://github.com/remvze/moodist/commit/2b843747b41111908bbe5fb8f5abc407114e4f15))
* decrease font size ([69cb45b](https://github.com/remvze/moodist/commit/69cb45bff74d36f654d521e9e7f6b2149b01d630))
* decrease opacity ([56802b6](https://github.com/remvze/moodist/commit/56802b67f2db751dbede9aa58b2158dd250a1420))
* decrease opacity ([2078648](https://github.com/remvze/moodist/commit/2078648c6687aab79a725490335b8ae751f3d4ee))
* decrease opacity ([82e4ea7](https://github.com/remvze/moodist/commit/82e4ea72f4ddb8658824813a64e14970400b1820))
* decrease padding ([98d2f76](https://github.com/remvze/moodist/commit/98d2f764383eaba0dd6163b93826459b614b67d2))
* decrease shine ([0f32de3](https://github.com/remvze/moodist/commit/0f32de3c0ca9f553c8917b786ddcdfb6feccf0c8))
* hide about and features ([400ea0a](https://github.com/remvze/moodist/commit/400ea0aeafe48587fe8596d1b5fe90755995d1c3))
* hide features ([9028675](https://github.com/remvze/moodist/commit/902867505743dd1dcd3f1e2afef010a186586615))
* increase dots ([405fccc](https://github.com/remvze/moodist/commit/405fcccd95d9ce720f0731e8040006bd1d9b8bd2))
* increase opacity ([882d440](https://github.com/remvze/moodist/commit/882d44079cfba8c7536c3713f0abeaa075ecb069))
* increase padding ([8e50013](https://github.com/remvze/moodist/commit/8e500136cec6ba5580146306f25a5956aa3a2a4b))
* increase pattern opacity ([5b83710](https://github.com/remvze/moodist/commit/5b83710c75515352b88c7bd361694d3067cb08fb))
* lower opacity ([d4cc24e](https://github.com/remvze/moodist/commit/d4cc24e468230df51e5676abbd828b2f2edd97f3))
* remove hero pattern ([8f36c86](https://github.com/remvze/moodist/commit/8f36c863d7f7489979691475947dbc8f29f26b39))
* revert changes ([341a896](https://github.com/remvze/moodist/commit/341a896924a6ace70114ad2ae3349f8934a329ba))
* revert pattern ([5916e86](https://github.com/remvze/moodist/commit/5916e86d3c6de9912b2c9bd490fa04cd9a0958dd))
* show about and features ([37505a6](https://github.com/remvze/moodist/commit/37505a6b3f86919ac04b69519e56ddbaf5234843))
### ✨ Features
* add about section ([d725d59](https://github.com/remvze/moodist/commit/d725d597034ead0bb63c5f0667b64ce459477662))
* add active indicators ([240fd9c](https://github.com/remvze/moodist/commit/240fd9c6e05c7385c0de92b8b57776988b902fae))
* add alarm for pomodoro timer ([0eb47ba](https://github.com/remvze/moodist/commit/0eb47ba2e1accaee7dd7d6114ca9a69cbc0656c4))
* add basic pomodoro structure ([9f7de33](https://github.com/remvze/moodist/commit/9f7de336e5add254b40c5694fc4c619ee00602ba))
* add controls to pomodoro ([7ed016d](https://github.com/remvze/moodist/commit/7ed016d8558a73d8d2bf05823cf80633882c1d69))
* add copy for productivity toolbox ([3205145](https://github.com/remvze/moodist/commit/3205145d5425c7a7a8660b46aa9de0b273a04ff0))
* add counter to notepad ([2424523](https://github.com/remvze/moodist/commit/24245235b14f9d59f86d2e988657a45a8a6d1eb7))
* add CTA button ([0e12a52](https://github.com/remvze/moodist/commit/0e12a5203ef836bd262b3d4cc02aaeb9048f461e))
* add custom presets ([2484e01](https://github.com/remvze/moodist/commit/2484e01273cf5e7ef5b44395cab26095891118fd))
* add dividers to menu items ([408734d](https://github.com/remvze/moodist/commit/408734d49fd89fbd47d29527c03927e49c834f30))
* add fade in/out ([663cb92](https://github.com/remvze/moodist/commit/663cb921350c083f1991665d147a3820bcdd9321))
* add features section ([e4e332a](https://github.com/remvze/moodist/commit/e4e332ad75aad1a58fd97acb71660b8dec9dfa09))
* add open-source section ([f7302de](https://github.com/remvze/moodist/commit/f7302dec5b7e182ad465bc30b63457a6e629a5b3))
* add scroll for lower heights ([758f2f4](https://github.com/remvze/moodist/commit/758f2f48dc6a4e520b7a1e937f96eed28c8e8c20))
* add simple notepad ([e923559](https://github.com/remvze/moodist/commit/e923559709796698257772cced4e20de584c6c80))
* add source code item ([d055e66](https://github.com/remvze/moodist/commit/d055e66dd9dd5789c88d1a002686457ea89db073))
* add special button ([a514a36](https://github.com/remvze/moodist/commit/a514a364ec5b6e2e34e7901ad51219d7be2aee86))
* add titles ([5f40435](https://github.com/remvze/moodist/commit/5f40435c0ccfec0cb87d9ac0c14723fb8265fa8d))
* add toolbar to notepad ([7463334](https://github.com/remvze/moodist/commit/7463334053ecd35a53cae535674169f5b50c81c2))
* change alignments ([1a01a00](https://github.com/remvze/moodist/commit/1a01a0086648c7564ecab30b79df0d67e93eb392))
* change the copy for features ([38da02a](https://github.com/remvze/moodist/commit/38da02a0d3b08e8f8802d6cf76a04ae656e10b76))
* change tooltip content ([941e1f0](https://github.com/remvze/moodist/commit/941e1f024189143340d663a0c117c08a0b315599))
* implement time setting ([f3cb2a1](https://github.com/remvze/moodist/commit/f3cb2a1b63e40f4f742ee2591b9353aa373f9783))
* persist pomodoro setting ([665e217](https://github.com/remvze/moodist/commit/665e2173f4083128687a6302a6f2fd82674f07c1))
* persist presets ([38a9a23](https://github.com/remvze/moodist/commit/38a9a23790248d5af522fc0d3cf6e99970e59637))
### 🐛 Bug Fixes
* add correct count to description ([81e6666](https://github.com/remvze/moodist/commit/81e66667765879da624544c5d915c1562f2ab34c))
* add key to categories ([38c11f1](https://github.com/remvze/moodist/commit/38c11f124e2235bc32de1e469b00ccaa22467a7e))
* add min-width to inputs ([dfd6c1f](https://github.com/remvze/moodist/commit/dfd6c1fc4a41845e686fc6ee96f71b696213fe69))
* change completion condition ([1ac5286](https://github.com/remvze/moodist/commit/1ac52861d1de651f8245d1e343307c6cf7a13dde))
* change default times ([158cffc](https://github.com/remvze/moodist/commit/158cffca8c4b138f33e2df03e046706d2b122478))
* change initial value ([a7e5368](https://github.com/remvze/moodist/commit/a7e53685918187c47d4fc2935418786b772c189e))
* change z-index values ([79afb8d](https://github.com/remvze/moodist/commit/79afb8d92f9cb8e551bf101267018af1ab58815d))
* comment out toolbox section ([a8a8c36](https://github.com/remvze/moodist/commit/a8a8c3643478d3da531e1da6c3640eb327acad3b))
* make sound count dynamic ([f66a6ff](https://github.com/remvze/moodist/commit/f66a6ffde770992353a5b21fe65c20fe50ab4328))
* remove extra headings ([7390a9b](https://github.com/remvze/moodist/commit/7390a9b3de0c52163d63b42ad48a882087886b65))
* remove fading ([653d309](https://github.com/remvze/moodist/commit/653d309e64b770c2b270d2435bcd02345b686fec))
* remove time from tabs array ([110356b](https://github.com/remvze/moodist/commit/110356b2da82e0f1e971ee9dc486664027d886ff))
* remove word counter dependency ([ae3ea8c](https://github.com/remvze/moodist/commit/ae3ea8c74f9a94ae56a0eb4b165bc36d990dea7b))
### [1.3.1](https://github.com/remvze/moodist/compare/v1.3.0...v1.3.1) (2024-02-01)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "moodist",
"version": "1.3.1",
"version": "1.4.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moodist",
"version": "1.3.1",
"version": "1.4.3",
"dependencies": {
"@astrojs/react": "^3.0.3",
"@floating-ui/react": "0.26.0",

View File

@@ -1,7 +1,7 @@
{
"name": "moodist",
"type": "module",
"version": "1.3.1",
"version": "1.4.3",
"scripts": {
"dev": "astro dev",
"start": "astro dev",

BIN
public/sounds/alarm.mp3 Normal file

Binary file not shown.

View File

@@ -0,0 +1,90 @@
.about {
padding-top: 10px;
& .effect {
position: sticky;
top: 0;
height: 80px;
background: linear-gradient(var(--color-neutral-50), transparent);
}
& .paragraph {
padding: 30px 0;
background: linear-gradient(
transparent,
var(--color-neutral-50) 10%,
var(--color-neutral-50) 90%,
transparent
);
&:last-of-type {
padding-bottom: 0;
}
& .counter {
width: max-content;
padding: 6px 16px;
margin-bottom: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-300);
border-radius: 20px 20px 20px 8px;
& span {
font-weight: 500;
color: var(--color-foreground);
}
}
& .title {
margin-bottom: 8px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .body {
line-height: 1.6;
color: var(--color-foreground-subtle);
}
}
.button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
margin-top: 20px;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border-radius: 50px;
outline: none;
transition: 0.2s;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
&:hover {
background-color: var(--color-neutral-100);
}
}
}

View File

@@ -0,0 +1,60 @@
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
import styles from './about.module.css';
export function About() {
const count = soundCount();
const paragraphs = [
{
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.',
title: 'Free Ambient Sounds',
},
{
body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`,
title: 'Carefully Curated Sounds',
},
{
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 goes beyond just ambient sounds by offering a suite of productivity tools to help you stay organized and focused. Utilize the built-in pomodoro timer to structure your workday in focused intervals, jot down thoughts and ideas in the simple notepad, and keep track of your tasks with the handy to-do list. These tools seamlessly integrate with the ambient soundscapes, allowing you to create a personalized environment that fosters both 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',
},
];
const handleClick = () => {
const app = document.getElementById('app');
app?.scrollIntoView();
};
return (
<section className={styles.about}>
<div className={styles.effect} />
<Container tight>
{paragraphs.map((paragraph, index) => (
<div className={styles.paragraph} key={index}>
<div className={styles.counter}>
<span>0{index + 1}</span> / 0{paragraphs.length}
</div>
<h2 className={styles.title}>{paragraph.title}</h2>
<p className={styles.body}>{paragraph.body}</p>
</div>
))}
<button className={styles.button} onClick={handleClick}>
Use Moodist
</button>
</Container>
</section>
);
}

View File

@@ -0,0 +1 @@
export { About } from './about';

View File

@@ -9,9 +9,8 @@ import { Container } from '@/components/container';
import { StoreConsumer } from '@/components/store-consumer';
import { Buttons } from '@/components/buttons';
import { Categories } from '@/components/categories';
import { ScrollToTop } from '@/components/scroll-to-top';
import { SharedModal } from '@/components/modals/shared';
import { Menu } from '@/components/menu/menu';
import { Toolbar } from '@/components/toolbar';
import { SnackbarProvider } from '@/contexts/snackbar';
import { sounds } from '@/data/sounds';
@@ -77,8 +76,7 @@ export function App() {
<Categories categories={allCategories} />
</Container>
<ScrollToTop />
<Menu />
<Toolbar />
<SharedModal />
</StoreConsumer>
</SnackbarProvider>

View File

@@ -13,15 +13,11 @@ export function Categories({ categories }: CategoriesProps) {
return (
<AnimatePresence initial={false}>
{categories.map((category, index) => (
<>
<Category
functional={category.id !== 'favorites'}
{...category}
key={category.id}
/>
<div key={category.id}>
<Category functional={category.id !== 'favorites'} {...category} />
{index === 3 && <Donate />}
</>
</div>
))}
</AnimatePresence>
);

View File

@@ -1,4 +1,6 @@
.donate {
margin-bottom: 20px;
& .iconContainer {
display: flex;
flex-direction: column;
@@ -38,26 +40,6 @@
}
.button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: max-content;
height: 40px;
padding: 0 20px;
margin: 16px auto 0;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-neutral-subtle);
text-decoration: none;
cursor: pointer;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 50px;
transition: 0.2s;
&:hover {
background-color: var(--color-neutral-100);
}
}
}

View File

@@ -1,5 +1,7 @@
import { FaCoffee } from 'react-icons/fa/index';
import { SpecialButton } from '@/components/special-button';
import styles from './donate.module.css';
export function Donate() {
@@ -12,16 +14,14 @@ export function Donate() {
</div>
</div>
<h2 className={styles.title}>Support Me</h2>
<div className={styles.title}>Support Me</div>
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
<a
<SpecialButton
className={styles.button}
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Donate Today
</a>
</SpecialButton>
</div>
);
}

View File

@@ -22,7 +22,7 @@ export function Category({
<div className={styles.icon}>{icon}</div>
</div>
<h2 className={styles.title}>{title}</h2>
<div className={styles.title}>{title}</div>
<Sounds functional={functional} id={id} sounds={sounds} />
</div>

View File

@@ -2,4 +2,12 @@
width: 90%;
max-width: 600px;
margin: 0 auto;
&.tight {
max-width: 450px;
}
&.wide {
max-width: 760px;
}
}

View File

@@ -1,9 +1,30 @@
import { cn } from '@/helpers/styles';
import styles from './container.module.css';
interface ContainerProps {
children: React.ReactNode;
className?: string;
tight?: boolean;
wide?: boolean;
}
export function Container({ children }: ContainerProps) {
return <div className={styles.container}>{children}</div>;
export function Container({
children,
className,
tight,
wide,
}: ContainerProps) {
return (
<div
className={cn(
styles.container,
className,
tight && styles.tight,
wide && styles.wide,
)}
>
{children}
</div>
);
}

View File

@@ -1,56 +0,0 @@
---
import { Container } from '@/components/container';
---
<Container>
<section class="wrapper">
<p class="text">
Enjoy Moodist? <a
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Support with a donation!
</a>
</p>
</section>
</Container>
<style>
.wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
&::after {
position: absolute;
bottom: 0;
left: 50%;
width: 80%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-200),
transparent
);
transform: translateX(-50%);
}
& .text {
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
.wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
&::after {
position: absolute;
bottom: 0;
left: 50%;
width: 80%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-200),
transparent
);
transform: translateX(-50%);
}
& .text {
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}

View File

@@ -0,0 +1,22 @@
import { Container } from '@/components/container';
import styles from './donate.module.css';
export function Donate() {
return (
<Container>
<section className={styles.wrapper}>
<p className={styles.text}>
Enjoy Moodist?{' '}
<a
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Support with a donation!
</a>
</p>
</section>
</Container>
);
}

View File

@@ -0,0 +1 @@
export { Donate } from './donate';

View File

@@ -0,0 +1,99 @@
.featuresSection {
margin-top: 40px;
& .iconContainer {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 15px;
& .tail {
width: 1px;
height: 75px;
background: linear-gradient(transparent, var(--color-neutral-300));
}
& .icon {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}
}
& .title {
margin-bottom: 8px;
font-family: var(--font-display);
font-size: var(--font-lg);
font-weight: 600;
text-align: center;
}
& .features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
row-gap: 32px;
column-gap: 20px;
margin-top: 24px;
& .icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin: 0 auto;
margin-bottom: 12px;
font-size: var(--font-md);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-200);
border-radius: 12px;
}
& .label {
margin-bottom: 8px;
font-family: var(--font-heading);
font-weight: 600;
text-align: center;
}
& .body {
width: 100%;
max-width: 275px;
margin: 0 auto;
line-height: 1.6;
color: var(--color-foreground-subtle);
text-align: center;
}
& .link {
display: block;
margin-top: 8px;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
text-align: center;
text-decoration: none;
}
& .soon {
display: flex;
width: max-content;
padding: 6px 12px;
margin: 0 auto;
margin-top: 8px;
font-size: var(--font-2xsm);
font-weight: 500;
line-height: 1;
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-300);
border-radius: 100px;
}
}
}

View File

@@ -0,0 +1,107 @@
import { BiMoney, BiUserCircle, BiLogoGithub } from 'react-icons/bi/index';
import { BsSoundwave, BsStars } from 'react-icons/bs/index';
import { RxMixerHorizontal } from 'react-icons/rx/index';
import { Balancer } from 'react-wrap-balancer';
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
import styles from './features.module.css';
export function Features() {
const count = soundCount();
const features = [
{
Icon: BiMoney,
body: 'Immerse yourself in sound without spending a dime.',
id: 'free-access',
label: 'Free Access',
},
{
Icon: BiUserCircle,
body: 'Dive right in, no sign-up hoops to jump through.',
id: 'no-registration',
label: 'No Registration',
},
{
Icon: BsSoundwave,
body: `Explore ${count} unique soundscapes, from rainforests to cityscapes.`,
id: 'diverse-sounds',
label: 'Diverse Sounds',
},
{
Icon: RxMixerHorizontal,
body: 'Craft your perfect soundscape by blending and adjusting sounds.',
id: 'customizable-mixes',
label: 'Customizable Mixes',
},
{
Icon: BiLogoGithub,
body: 'Contribute and collaborate, making the best even better.',
id: 'open-source',
label: 'Open-Source',
link: {
label: 'Source Code',
url: 'https://github.com/remvze/moodist',
},
},
{
Icon: BsStars,
body: 'Uninterrupted immersion, focus on the sounds, not the tech.',
id: 'seamless-experience',
label: 'Seamless Experience',
},
{
Icon: BsStars,
body: 'Spread the calm, easily share your customized sound blends.',
id: 'share-selections',
label: 'Share Selections',
},
{
Icon: BsStars,
body: 'Lock in your favorite mixes for instant return to your sonic haven.',
id: 'save-presets',
label: 'Save Presets',
soon: true,
},
];
return (
<section className={styles.featuresSection}>
<Container>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>
<BsStars />
</div>
</div>
<h2 className={styles.title}>Features</h2>
<div className={styles.features}>
{features.map(feature => (
<div className={styles.reason} key={feature.id}>
<div className={styles.icon}>
<feature.Icon />
</div>
<h3 className={styles.label}>{feature.label}</h3>
<p className={styles.body}>
<Balancer>{feature.body}</Balancer>
</p>
{feature.link && (
<a className={styles.link} href={feature.link.url}>
{feature.link.label}
</a>
)}
{feature.soon && <div className={styles.soon}>Coming Soon</div>}
</div>
))}
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1 @@
export { Features } from './features';

View File

@@ -1,38 +0,0 @@
---
import { Container } from '@/components/container';
---
<footer class="footer">
<Container>
<p>
Created by{' '}
<a href="https://twitter.com/remvze">
Maze <span>✦</span>
</a>
</p>
</Container>
</footer>
<style>
.footer {
display: flex;
align-items: center;
height: 100px;
& p {
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
& span {
color: #c0eb75;
}
}
}
}
</style>

View File

@@ -0,0 +1,17 @@
.footer {
display: flex;
align-items: center;
height: 100px;
& p {
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}

View File

@@ -0,0 +1,15 @@
import { Container } from '@/components/container';
import styles from './footer.module.css';
export function Footer() {
return (
<footer className={styles.footer}>
<Container>
<p>
Created by <a href="https://twitter.com/remvze">Maze </a>
</p>
</Container>
</footer>
);
}

View File

@@ -0,0 +1 @@
export { Footer } from './footer';

View File

@@ -1,131 +0,0 @@
---
import { Balancer } from 'react-wrap-balancer';
import { BsSoundwave } from 'react-icons/bs/index';
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
---
<div class="hero">
<Container>
<img
alt="Faded Moodist Logo"
class="logo"
height={45}
src="/logo.svg"
width={45}
/>
<div class="title">
<div class="left"></div>
<h1>Moodist</h1>
<div class="right"></div>
</div>
<p class="desc">
<Balancer>Ambient sounds for focus and calm.</Balancer>
</p>
<p class="sounds">
<span class="icon"><BsSoundwave /></span>
<span>{count} Sounds</span>
</p>
</Container>
</div>
<style>
.hero {
padding: 100px 0 60px;
text-align: center;
& .logo {
display: block;
width: 45px;
margin: 0 auto 12px;
}
& .title {
display: flex;
column-gap: 15px;
align-items: center;
& div {
flex-grow: 1;
height: 1px;
&.left {
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300)
);
}
&.right {
background: linear-gradient(
90deg,
var(--color-neutral-300),
transparent
);
}
}
& h1 {
font-family: var(--font-display);
font-size: var(--font-2xlg);
font-weight: 600;
}
}
& .desc {
margin-top: 5px;
line-height: 1.6;
color: var(--color-foreground-subtle);
}
& .sounds {
position: relative;
display: flex;
column-gap: 8px;
align-items: center;
justify-content: center;
width: max-content;
height: 28px;
padding-right: 12px;
margin: 20px auto 0;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-200);
border-radius: 100px;
& .icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 8px 0 12px;
color: var(--color-foreground);
border-right: 1px solid var(--color-neutral-200);
}
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);
}
}
}
</style>

View File

@@ -0,0 +1,120 @@
.hero {
text-align: center;
.container {
position: relative;
padding: 100px 0 80px;
/* padding: 120px 0 60px; */
& .pattern {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-image: radial-gradient(
var(--color-neutral-300) 5%,
transparent 5%
);
background-position: top center;
background-size: 31px 31px;
opacity: 0.9;
mask-image: linear-gradient(#fff, transparent, transparent);
}
}
& .logo {
display: block;
width: 45px;
margin: 0 auto 12px;
}
& .title {
display: flex;
column-gap: 15px;
align-items: center;
& 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;
}
}
& .desc {
margin-top: 5px;
line-height: 1.6;
color: var(--color-foreground-subtle);
}
& .sounds {
position: relative;
display: flex;
column-gap: 8px;
align-items: center;
justify-content: center;
width: max-content;
height: 28px;
padding-right: 12px;
margin: 20px auto 0;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-200);
border-radius: 100px;
& .icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 10px;
color: var(--color-foreground);
border-right: 1px solid var(--color-neutral-200);
border-radius: 0 100px 100px 0;
}
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);
}
}
}

View File

@@ -0,0 +1,42 @@
import { useMemo } from 'react';
import { BsSoundwave } from 'react-icons/bs/index';
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
import styles from './hero.module.css';
export function Hero() {
const count = useMemo(soundCount, []);
return (
<div className={styles.hero}>
<Container className={styles.container}>
{/* <div className={styles.pattern} /> */}
<img
alt="Faded Moodist Logo"
className={styles.logo}
height={45}
src="/logo.svg"
width={45}
/>
<div className={styles.title}>
<div className={styles.left}></div>
<h2>Moodist</h2>
<div className={styles.right}></div>
</div>
<h1 className={styles.desc}>Ambient sounds for focus and calm.</h1>
<p className={styles.sounds}>
<span className={styles.icon}>
<BsSoundwave />
</span>
<span>{count} Sounds</span>
</p>
</Container>
</div>
);
}

View File

@@ -0,0 +1 @@
export { Hero } from './hero';

View File

@@ -0,0 +1,6 @@
.divider {
width: 100%;
height: 1px;
min-height: 1px;
background-color: var(--color-neutral-200);
}

View File

@@ -0,0 +1,5 @@
import styles from './divider.module.css';
export function Divider() {
return <div className={styles.divider} />;
}

View File

@@ -0,0 +1 @@
export { Divider } from './divider';

View File

@@ -4,16 +4,17 @@
align-items: center;
justify-content: flex-start;
width: 100%;
padding: 16px 12px;
height: 40px;
min-height: 40px;
padding: 0 12px;
font-size: var(--font-sm);
font-weight: 500;
line-height: 1;
color: var(--color-foreground-subtle);
text-align: left;
text-decoration: none;
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border: 1px solid transparent;
border-radius: 4px;
outline: none;
transition: 0.2s;
@@ -26,9 +27,17 @@
&:not(:disabled):hover {
color: var(--color-foreground);
background-color: var(--color-neutral-200);
border: 1px solid var(--color-neutral-300);
}
& .icon {
color: var(--color-foreground);
}
& .active {
width: 4px;
height: 4px;
background: var(--color-neutral-950);
border-radius: 50%;
}
}

View File

@@ -1,6 +1,7 @@
import styles from './item.module.css';
interface ItemProps {
active?: boolean;
disabled?: boolean;
href?: string;
icon: React.ReactElement;
@@ -9,6 +10,7 @@ interface ItemProps {
}
export function Item({
active,
disabled = false,
href,
icon,
@@ -25,6 +27,7 @@ export function Item({
{...(href ? { href, target: '_blank' } : {})}
>
<span className={styles.icon}>{icon}</span> {label}
{active && <div className={styles.active} />}
</Comp>
);
}

View File

@@ -1,3 +1,7 @@
export { Shuffle as ShuffleItem } from './shuffle';
export { Share as ShareItem } from './share';
export { Donate as DonateItem } from './donate';
export { Notepad as NotepadItem } from './notepad';
export { Source as SourceItem } from './source';
export { Pomodoro as PomodoroItem } from './pomodoro';
export { Presets as PresetsItem } from './presets';

View File

@@ -0,0 +1,22 @@
import { MdNotes } from 'react-icons/md/index';
import { Item } from '../item';
import { useNoteStore } from '@/store';
interface NotepadProps {
open: () => void;
}
export function Notepad({ open }: NotepadProps) {
const note = useNoteStore(state => state.note);
return (
<Item
active={!!note.length}
icon={<MdNotes />}
label="Notepad"
onClick={open}
/>
);
}

View File

@@ -0,0 +1,22 @@
import { MdOutlineAvTimer } from 'react-icons/md/index';
import { Item } from '../item';
import { usePomodoroStore } from '@/store';
interface PomodoroProps {
open: () => void;
}
export function Pomodoro({ open }: PomodoroProps) {
const running = usePomodoroStore(state => state.running);
return (
<Item
active={running}
icon={<MdOutlineAvTimer />}
label="Pomodoro"
onClick={open}
/>
);
}

View File

@@ -0,0 +1,11 @@
import { RiPlayListFill } from 'react-icons/ri/index';
import { Item } from '../item';
interface PresetsProps {
open: () => void;
}
export function Presets({ open }: PresetsProps) {
return <Item icon={<RiPlayListFill />} label="Your Presets" onClick={open} />;
}

View File

@@ -0,0 +1,13 @@
import { LuGithub } from 'react-icons/lu/index';
import { Item } from '../item';
export function Source() {
return (
<Item
href="https://github.com/remvze/moodist"
icon={<LuGithub />}
label="Source Code"
/>
);
}

View File

@@ -1,9 +1,4 @@
.wrapper {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 5;
& .menuButton {
display: flex;
align-items: center;
@@ -28,30 +23,11 @@
flex-direction: column;
row-gap: 4px;
width: 240px;
height: max-content;
padding: 4px;
overflow: auto;
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 4px;
& .menuItem {
position: flex;
align-items: center;
width: 100%;
padding: 12px 8px;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground-subtle);
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
outline: none;
transition: 0.2s;
&:hover {
color: var(--color-foreground);
background-color: var(--color-neutral-200);
}
}
}
}

View File

@@ -1,12 +1,12 @@
import { useState } from 'react';
import { IoMenu, IoClose } from 'react-icons/io5/index';
import { AnimatePresence, motion } from 'framer-motion';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
size,
useClick,
useDismiss,
useRole,
@@ -14,22 +14,44 @@ import {
FloatingFocusManager,
} from '@floating-ui/react';
import { ShuffleItem, ShareItem, DonateItem } from './items';
import {
ShuffleItem,
ShareItem,
DonateItem,
NotepadItem,
SourceItem,
PomodoroItem,
PresetsItem,
} from './items';
import { Divider } from './divider';
import { ShareLinkModal } from '@/components/modals/share-link';
import { slideY, fade, mix } from '@/lib/motion';
import { PresetsModal } from '@/components/modals/presets';
import { Notepad, Pomodoro } from '@/components/toolbox';
import styles from './menu.module.css';
export function Menu() {
const [isOpen, setIsOpen] = useState(false);
const [showPresets, setShowPresets] = useState(false);
const [showShareLink, setShowShareLink] = useState(false);
const variants = mix(slideY(-20), fade());
const [showNotepad, setShowNotepad] = useState(false);
const [showPomodoro, setShowPomodoro] = useState(false);
const { context, floatingStyles, refs } = useFloating({
middleware: [offset(12), flip(), shift()],
middleware: [
offset(12),
flip(),
shift(),
size({
apply({ availableHeight, elements }) {
Object.assign(elements.floating.style, {
maxHeight: `${availableHeight}px`,
});
},
padding: 10,
}),
],
onOpenChange: setIsOpen,
open: isOpen,
placement: 'top-end',
@@ -59,35 +81,37 @@ export function Menu() {
{isOpen ? <IoClose /> : <IoMenu />}
</button>
<AnimatePresence>
{isOpen && (
<FloatingFocusManager context={context} modal={false}>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
>
<motion.div
animate="show"
className={styles.menu}
exit="hidden"
initial="hidden"
variants={variants}
>
<ShareItem open={() => setShowShareLink(true)} />
<ShuffleItem />
<DonateItem />
</motion.div>
</div>
</FloatingFocusManager>
)}
</AnimatePresence>
{isOpen && (
<FloatingFocusManager context={context} modal={false}>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className={styles.menu}
>
<PresetsItem open={() => setShowPresets(true)} />
<ShareItem open={() => setShowShareLink(true)} />
<ShuffleItem />
<Divider />
<NotepadItem open={() => setShowNotepad(true)} />
<PomodoroItem open={() => setShowPomodoro(true)} />
<Divider />
<DonateItem />
<SourceItem />
</div>
</FloatingFocusManager>
)}
</div>
<ShareLinkModal
show={showShareLink}
onClose={() => setShowShareLink(false)}
/>
<PresetsModal show={showPresets} onClose={() => setShowPresets(false)} />
<Notepad show={showNotepad} onClose={() => setShowNotepad(false)} />
<Pomodoro show={showPomodoro} onClose={() => setShowPomodoro(false)} />
</>
);
}

View File

@@ -1,7 +1,7 @@
.overlay {
position: fixed;
inset: 0;
z-index: 10;
z-index: 20;
background-color: rgb(9 9 11 / 40%);
backdrop-filter: blur(5px);
}
@@ -10,7 +10,7 @@
position: fixed;
top: 50%;
left: 50%;
z-index: 12;
z-index: 20;
width: 100%;
max-height: 100%;
padding: 50px 0;
@@ -29,6 +29,13 @@
background-color: var(--color-neutral-100);
border-radius: 8px;
&.wide {
width: 95%;
max-width: 600px;
padding: 12px;
padding-top: 40px;
}
& .close {
position: absolute;
top: 10px;

View File

@@ -1,52 +1,74 @@
import { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { IoClose } from 'react-icons/io5/index';
import { Portal } from '@/components/portal';
import { fade, mix, slideY } from '@/lib/motion';
import { cn } from '@/helpers/styles';
import styles from './modal.module.css';
interface ModalProps {
children: React.ReactNode;
lockBody?: boolean;
onClose: () => void;
show: boolean;
wide?: boolean;
}
export function Modal({ children, onClose, show }: ModalProps) {
export function Modal({
children,
lockBody = true,
onClose,
show,
wide,
}: ModalProps) {
const variants = {
modal: mix(fade(), slideY(20)),
overlay: fade(),
};
useEffect(() => {
if (show && lockBody) {
document.body.style.overflow = 'hidden';
} else if (lockBody) {
document.body.style.overflow = 'auto';
}
}, [show, lockBody]);
return (
<AnimatePresence>
{show && (
<>
<motion.div
animate="show"
className={styles.overlay}
exit="hidden"
initial="hidden"
variants={variants.overlay}
onClick={onClose}
onKeyDown={onClose}
/>
<div className={styles.modal}>
<Portal>
<AnimatePresence>
{show && (
<>
<motion.div
animate="show"
className={styles.content}
className={styles.overlay}
exit="hidden"
initial="hidden"
variants={variants.modal}
>
<button className={styles.close} onClick={onClose}>
<IoClose />
</button>
variants={variants.overlay}
onClick={onClose}
onKeyDown={onClose}
/>
<div className={styles.modal}>
<motion.div
animate="show"
className={cn(styles.content, wide && styles.wide)}
exit="hidden"
initial="hidden"
variants={variants.modal}
>
<button className={styles.close} onClick={onClose}>
<IoClose />
</button>
{children}
</motion.div>
</div>
</>
)}
</AnimatePresence>
{children}
</motion.div>
</div>
</>
)}
</AnimatePresence>
</Portal>
);
}

View File

@@ -0,0 +1 @@
export { PresetsModal } from './presets';

View File

@@ -0,0 +1 @@
export { List } from './list';

View File

@@ -0,0 +1,62 @@
.list {
& .title {
margin-bottom: 8px;
font-weight: 500;
color: var(--color-foreground-subtle);
}
& .empty {
font-size: var(--font-sm);
}
& .preset {
display: flex;
column-gap: 4px;
align-items: center;
width: 100%;
height: 45px;
padding: 4px;
margin-top: 8px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
&:not(:last-of-type) {
margin-bottom: 8px;
}
& input {
flex-grow: 1;
min-width: 0;
height: 100%;
padding: 0 12px;
color: var(--color-foreground);
background: transparent;
border: none;
outline: none;
}
& button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
aspect-ratio: 1 / 1;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground-subtle);
cursor: pointer;
background-color: var(--color-neutral-100);
border: none;
border-radius: 4px;
outline: none;
&.primary {
font-size: var(--font-xsm);
color: var(--color-foreground);
background-color: var(--color-neutral-200);
border: 1px solid var(--color-neutral-300);
}
}
}
}

View File

@@ -0,0 +1,53 @@
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
import styles from './list.module.css';
import { usePresetStore, useSoundStore } from '@/store';
interface ListProps {
close: () => void;
}
export function List({ close }: ListProps) {
const presets = usePresetStore(state => state.presets);
const changeName = usePresetStore(state => state.changeName);
const deletePreset = usePresetStore(state => state.deletePreset);
const override = useSoundStore(state => state.override);
const play = useSoundStore(state => state.play);
return (
<div className={styles.list}>
<h3 className={styles.title}>
Your Presets {presets.length > 0 && `(${presets.length})`}
</h3>
{!presets.length && (
<p className={styles.empty}>You don&apos;t have any presets yet.</p>
)}
{presets.map((preset, index) => (
<div className={styles.preset} key={index}>
<input
placeholder="Untitled"
type="text"
value={preset.label}
onChange={e => changeName(index, e.target.value)}
/>
<button onClick={() => deletePreset(index)}>
<FaRegTrashAlt />
</button>
<button
className={styles.primary}
onClick={() => {
override(preset.sounds);
play();
close();
}}
>
<FaPlay />
</button>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1 @@
export { New } from './new';

View File

@@ -0,0 +1,62 @@
.new {
margin-top: 16px;
& .title {
font-weight: 500;
color: var(--color-foreground-subtle);
}
& .form {
display: flex;
align-items: center;
width: 100%;
height: 45px;
padding: 4px;
margin-top: 8px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
&.disabled {
filter: blur(2px);
opacity: 0.4;
}
& input {
flex-grow: 1;
min-width: 0;
height: 100%;
padding: 0 12px;
color: var(--color-foreground);
background: transparent;
border: none;
outline: none;
}
& button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 12px;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-neutral-50);
cursor: pointer;
background-color: var(--color-neutral-950);
border: none;
border-radius: 4px;
outline: none;
&:disabled {
cursor: not-allowed;
}
}
}
& .noSelected {
margin-top: 8px;
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
}
}

View File

@@ -0,0 +1,59 @@
import { useState, type FormEvent } from 'react';
import { cn } from '@/helpers/styles';
import { useSoundStore, usePresetStore } from '@/store';
import styles from './new.module.css';
export function New() {
const [name, setName] = useState('');
const noSelected = useSoundStore(state => state.noSelected());
const sounds = useSoundStore(state => state.sounds);
const addPreset = usePresetStore(state => state.addPreset);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!name || noSelected) return;
const _sounds: Record<string, number> = {};
Object.keys(sounds)
.filter(id => sounds[id].isSelected)
.forEach(id => {
_sounds[id] = sounds[id].volume;
});
addPreset(name, _sounds);
setName('');
};
return (
<div className={styles.new}>
<h3 className={styles.title}>New Preset</h3>
<form
className={cn(styles.form, noSelected && styles.disabled)}
onSubmit={handleSubmit}
>
<input
disabled={noSelected}
placeholder="Preset's Name"
required
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
<button disabled={noSelected}>Save</button>
</form>
{noSelected && (
<p className={styles.noSelected}>
To make a preset, first select some sounds.
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,12 @@
.title {
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
.divider {
width: 100%;
height: 1px;
margin: 16px 0;
background-color: var(--color-neutral-200);
}

View File

@@ -0,0 +1,21 @@
import { Modal } from '@/components/modal';
import { New } from './new';
import { List } from './list';
import styles from './presets.module.css';
interface PresetsModalProps {
onClose: () => void;
show: boolean;
}
export function PresetsModal({ onClose, show }: PresetsModalProps) {
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Presets</h2>
<New />
<div className={styles.divider} />
<List close={onClose} />
</Modal>
);
}

View File

@@ -23,6 +23,7 @@
& input {
flex-grow: 1;
min-width: 0;
height: 100%;
padding: 0 10px;
font-size: var(--font-sm);

View File

@@ -0,0 +1 @@
export { Portal } from './portal';

View File

@@ -0,0 +1,14 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
interface PortalProps {
children: React.ReactNode;
}
export function Portal({ children }: PortalProps) {
const [isClientSide, setIsClientSide] = useState(false);
useEffect(() => setIsClientSide(true), []);
return isClientSide ? createPortal(children, document.body) : null;
}

View File

@@ -1,8 +1,4 @@
.button {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 99;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -31,7 +31,7 @@ export function ScrollToTop() {
return (
<AnimatePresence>
{isVisible && (
{isVisible ? (
<motion.button
animate="show"
aria-label="Scroll to top"
@@ -43,6 +43,8 @@ export function ScrollToTop() {
>
<BiUpArrowAlt />
</motion.button>
) : (
<div />
)}
</AnimatePresence>
);

View File

@@ -1,62 +0,0 @@
---
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
---
<div class="about">
<Container>
<div class="titleWrapper">
<h2 class="title">What is Moodist?</h2>
<div class="line"></div>
</div>
<p class="desc">
Welcome to Moodist your free, open-source ambient sound generator. With <span
>{count} curated sounds</span
>, effortlessly create your custom mix for focus or relaxation. No
accounts, no hassle just pure tranquility. Explore nature&apos;s calm
and urban rhythms. Elevate your ambiance with Moodist, where simplicity
meets serenity.
</p>
</Container>
</div>
<style>
.about {
padding: 80px 0;
& .titleWrapper {
display: flex;
column-gap: 12px;
align-items: center;
& .title {
margin-bottom: 12px;
font-family: var(--font-display);
font-size: var(--font-lg);
font-weight: 600;
}
& .line {
flex-grow: 1;
height: 1px;
background: linear-gradient(
90deg,
var(--color-neutral-300),
transparent
);
transform: translateY(-0.25rem);
}
}
& .desc {
line-height: 1.7;
color: var(--color-foreground-subtle);
& span {
color: var(--color-foreground);
}
}
}
</style>

View File

@@ -1,121 +0,0 @@
---
import { RiSparkling2Line } from 'react-icons/ri/index';
import { Container } from '@/components/container';
---
<div class="ready">
<Container>
<div class="wrapper">
<div class="iconContainer">
<div class="tail"></div>
<div class="icon">
<RiSparkling2Line />
</div>
</div>
<h2 class="title">Are you ready?</h2>
<p class="desc">Create your calm oasis in seconds!</p>
<button class="button" id="button"> Use Moodist</button>
</div>
</Container>
</div>
<script>
const button = document.getElementById('button');
button?.addEventListener('click', () => {
const app = document.getElementById('app');
app?.scrollIntoView(true);
});
</script>
<style>
.ready {
& .wrapper {
position: relative;
padding: 0 20px 40px;
background: linear-gradient(transparent, var(--color-neutral-100));
border-radius: 0 0 20px 20px;
&::after {
position: absolute;
bottom: 0;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
}
& .iconContainer {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 15px;
& .tail {
width: 1px;
height: 75px;
background: linear-gradient(transparent, var(--color-neutral-300));
}
& .icon {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}
}
& .title {
margin-bottom: 12px;
font-family: var(--font-display);
font-size: var(--font-lg);
font-weight: 600;
text-align: center;
}
& .desc {
color: var(--color-foreground-subtle);
text-align: center;
}
& .button {
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 40px;
margin: 24px auto 0;
font-family: var(--font-heading);
font-size: var(--font-sm);
line-height: 0;
color: var(--color-neutral-200);
text-decoration: none;
cursor: pointer;
background-color: var(--color-neutral-950);
border: none;
border-radius: 100px;
outline: none;
transition: 0.2s;
&:hover {
background-color: var(--color-neutral-800);
}
}
}
</style>

View File

@@ -1,157 +0,0 @@
---
import { BiMoney, BiUserCircle, BiLogoGithub } from 'react-icons/bi/index';
import { BsSoundwave, BsStars } from 'react-icons/bs/index';
import { RxMixerHorizontal } from 'react-icons/rx/index';
import { Balancer } from 'react-wrap-balancer';
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
const reasons = [
{
Icon: BiMoney,
body: "Immerse yourself in Moodist's ambient world without spending a dime. All features are accessible to everyone, ensuring a cost-free auditory journey.",
id: 'free-access',
label: 'Free Access',
},
{
Icon: BiUserCircle,
body: 'Embrace simplicity Moodist skips the registration process. No accounts, no hassle; just click, play, and enjoy the serenity.',
id: 'no-registration',
label: 'No Registration',
},
{
Icon: BsSoundwave,
body: `With a curated collection of ${count} sounds, Moodist offers a spectrum of auditory experiences. From the tranquility of nature to the beat of urban life, find the perfect backdrop for your mood.`,
id: 'diverse-sounds',
label: 'Diverse Sounds',
},
{
Icon: RxMixerHorizontal,
body: 'Tailor your ambiance effortlessly. Moodist allows you to create personalized mixes, adjusting the blend of sounds to suit your focus or relaxation needs.',
id: 'customizable-mixes',
label: 'Customizable Mixes',
},
{
Icon: BiLogoGithub,
body: 'Trust in transparency. Moodist is open-source, fostering collaboration and providing users with a platform they can explore and understand.',
id: 'open-source',
label: 'Open-Source',
link: {
label: 'Source Code',
url: 'https://github.com/remvze/moodist',
},
},
{
Icon: BsStars,
body: 'Navigate with ease. Moodist provides a user-friendly interface, ensuring a smooth and hassle-free experience as you explore the diverse soundscape of calm and rhythm.',
id: 'seamless-experience',
label: 'Seamless Experience',
},
];
---
<div class="why">
<Container>
<div class="titleWrapper">
<h2 class="title">Why use Moodist?</h2>
<div class="line"></div>
</div>
<div class="reasons">
{
reasons.map(reason => (
<div class="reason">
<div class="icon">
<reason.Icon />
</div>
<h3 class="label">{reason.label}</h3>
<p class="body">
<Balancer>{reason.body}</Balancer>
</p>
{reason.link && (
<a class="link" href={reason.link.url}>
{reason.link.label} →
</a>
)}
</div>
))
}
</div>
</Container>
</div>
<style>
.why {
padding-bottom: 80px;
& .titleWrapper {
display: flex;
column-gap: 12px;
align-items: center;
& .title {
margin-bottom: 12px;
font-family: var(--font-display);
font-size: var(--font-lg);
font-weight: 600;
}
& .line {
flex-grow: 1;
height: 1px;
background: linear-gradient(
90deg,
var(--color-neutral-300),
transparent
);
transform: translateY(-0.25rem);
}
}
& .reasons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
row-gap: 28px;
column-gap: 20px;
margin-top: 24px;
& .icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-bottom: 12px;
font-size: var(--font-md);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-200);
border-radius: 12px;
}
& .label {
margin-bottom: 8px;
font-family: var(--font-heading);
font-weight: 600;
}
& .body {
line-height: 1.6;
color: var(--color-foreground-subtle);
}
& .link {
display: block;
margin-top: 8px;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}
</style>

View File

@@ -91,7 +91,7 @@
}
}
& h3 {
& .label {
margin-top: 8px;
font-family: var(--font-heading);
font-size: var(--font-sm);

View File

@@ -90,7 +90,9 @@ export function Sound({
icon
)}
</div>
<h3 id={id}>{label}</h3>
<div className={styles.label} id={id}>
{label}
</div>
<Range id={id} />
</div>
);

View File

@@ -0,0 +1 @@
export { Source } from './source';

View File

@@ -0,0 +1,70 @@
.source {
/* margin-top: 80px; */
margin-top: 40px;
& .wrapper {
position: relative;
padding: 0 20px 40px;
background: linear-gradient(transparent, rgb(24 24 27 / 70%));
border-radius: 0 0 20px 20px;
&::after {
position: absolute;
bottom: 0;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);
}
}
& .iconContainer {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 15px;
& .tail {
width: 1px;
height: 75px;
background: linear-gradient(transparent, var(--color-neutral-300));
}
& .icon {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}
}
& .title {
font-family: var(--font-display);
font-size: var(--font-lg);
font-weight: 600;
text-align: center;
}
& .desc {
margin-top: 8px;
color: var(--color-foreground-subtle);
text-align: center;
}
.button {
margin: 16px auto 0;
}
}

View File

@@ -0,0 +1,32 @@
import { FaGithub } from 'react-icons/fa/index';
import { Container } from '@/components/container';
import { SpecialButton } from '@/components/special-button';
import styles from './source.module.css';
export function Source() {
return (
<div className={styles.source}>
<Container>
<div className={styles.wrapper}>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>
<FaGithub />
</div>
</div>
<h2 className={styles.title}>Open Source</h2>
<p className={styles.desc}>Moodist is free and open-source!</p>
<SpecialButton
className={styles.button}
href="https://github.com/remvze/moodist"
>
Source Code
</SpecialButton>
</div>
</Container>
</div>
);
}

View File

@@ -0,0 +1 @@
export { SpecialButton } from './special-button';

View File

@@ -0,0 +1,74 @@
.button {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: max-content;
height: 40px;
padding: 0 20px;
overflow: hidden;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-neutral-subtle);
text-decoration: none;
cursor: pointer;
background-color: var(--color-neutral-200);
border: none;
border-radius: 50px;
transition: 0.2s;
&::after {
position: absolute;
top: 1px;
left: 1px;
z-index: -1;
width: calc(100% - 2px);
height: calc(100% - 2px);
content: '';
background-color: var(--color-neutral-50);
border-radius: 100px;
transition: inherit;
}
&::before {
position: absolute;
top: 50%;
left: 50%;
z-index: -2;
width: 150%;
aspect-ratio: 1 / 1;
content: '';
background-image: conic-gradient(
transparent,
transparent,
var(--color-neutral-400),
transparent,
transparent,
transparent,
transparent,
var(--color-neutral-400),
transparent,
transparent
);
transform: translate(-50%, -50%);
animation-name: shine;
animation-duration: 6s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
&:hover::after {
background-color: var(--color-neutral-100);
}
}
@keyframes shine {
0% {
transform: translate(-50%, -50%) rotate(90deg);
}
100% {
transform: translate(-50%, -50%) rotate(450deg);
}
}

View File

@@ -0,0 +1,26 @@
import { cn } from '@/helpers/styles';
import styles from './special-button.module.css';
interface SpecialButtonProps {
children: React.ReactNode;
className?: string;
href: string;
}
export function SpecialButton({
children,
className,
href,
}: SpecialButtonProps) {
return (
<a
className={cn(styles.button, className)}
href={href}
rel="noreferrer"
target="_blank"
>
{children}
</a>
);
}

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useSoundStore } from '@/store';
import { useSoundStore, useNoteStore, usePresetStore } from '@/store';
interface StoreConsumerProps {
children: React.ReactNode;
@@ -9,6 +9,8 @@ interface StoreConsumerProps {
export function StoreConsumer({ children }: StoreConsumerProps) {
useEffect(() => {
useSoundStore.persist.rehydrate();
useNoteStore.persist.rehydrate();
usePresetStore.persist.rehydrate();
}, []);
return <>{children}</>;

View File

@@ -0,0 +1 @@
export { Toolbar } from './toolbar';

View File

@@ -0,0 +1,13 @@
.wrapper {
position: fixed;
bottom: 20px;
left: 0;
z-index: 15;
width: 100%;
.container {
display: flex;
align-items: center;
justify-content: space-between;
}
}

View File

@@ -0,0 +1,16 @@
import { Container } from '@/components/container';
import { Menu } from '@/components/menu';
import { ScrollToTop } from '@/components/scroll-to-top';
import styles from './toolbar.module.css';
export function Toolbar() {
return (
<div className={styles.wrapper}>
<Container className={styles.container} wide>
<ScrollToTop />
<Menu />
</Container>
</div>
);
}

View File

@@ -0,0 +1,2 @@
export { Notepad } from './notepad';
export { Pomodoro } from './pomodoro';

View File

@@ -0,0 +1,39 @@
.button {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: var(--font-sm);
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
outline: none;
transition: 0.2s;
transition-property: border-color, color, background-color;
&.critical {
color: #f43f5e;
border-color: #f43f5e;
&:hover {
background-color: rgb(244 63 94 / 10%);
}
}
&.recommended {
font-size: var(--font-xsm);
color: #22c55e;
border-color: #22c55e;
&:hover {
background-color: rgb(34 197 94 / 10%);
}
}
&:hover {
background-color: var(--color-neutral-200);
}
}

View File

@@ -0,0 +1,36 @@
import { Tooltip } from '@/components/tooltip';
import { cn } from '@/helpers/styles';
import styles from './button.module.css';
interface ButtonProps {
critical?: boolean;
icon: React.ReactElement;
onClick: () => void;
recommended?: boolean;
tooltip: string;
}
export function Button({
critical,
icon,
onClick,
recommended,
tooltip,
}: ButtonProps) {
return (
<Tooltip content={tooltip} hideDelay={0} placement="bottom" showDelay={0}>
<button
className={cn(
styles.button,
critical && styles.critical,
recommended && styles.recommended,
)}
onClick={onClick}
>
{icon}
</button>
</Tooltip>
);
}

View File

@@ -0,0 +1 @@
export { Button } from './button';

View File

@@ -0,0 +1 @@
export { Notepad } from './notepad';

View File

@@ -0,0 +1,39 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
& .label {
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground-subtle);
}
& .buttons {
display: flex;
column-gap: 4px;
align-items: center;
}
}
.textarea {
width: 100%;
height: 350px;
padding: 12px;
line-height: 1.6;
color: var(--color-foreground-subtle);
resize: none;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
outline: none;
scroll-padding-bottom: 12px;
}
.counter {
margin-top: 8px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
text-align: center;
}

View File

@@ -0,0 +1,70 @@
import { BiTrash } from 'react-icons/bi/index';
import { LuCopy, LuDownload } from 'react-icons/lu/index';
import { FaCheck } from 'react-icons/fa6/index';
import { FaUndo } from 'react-icons/fa/index';
import { Modal } from '@/components/modal';
import { Button } from './button';
import { useNoteStore } from '@/store';
import { useCopy } from '@/hooks/use-copy';
import { download } from '@/helpers/download';
import styles from './notepad.module.css';
interface NotepadProps {
onClose: () => void;
show: boolean;
}
export function Notepad({ onClose, show }: NotepadProps) {
const note = useNoteStore(state => state.note);
const history = useNoteStore(state => state.history);
const write = useNoteStore(state => state.write);
const words = useNoteStore(state => state.words());
const characters = useNoteStore(state => state.characters());
const clear = useNoteStore(state => state.clear);
const restore = useNoteStore(state => state.restore);
const { copy, copying } = useCopy();
return (
<Modal show={show} wide onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.label}>Your Note</h2>
<div className={styles.buttons}>
<Button
icon={copying ? <FaCheck /> : <LuCopy />}
tooltip="Copy Note"
onClick={() => copy(note)}
/>
<Button
icon={<LuDownload />}
tooltip="Download Note"
onClick={() => download('Moodit Note.txt', note)}
/>
<Button
critical={!history}
icon={history ? <FaUndo /> : <BiTrash />}
recommended={!!history}
tooltip={history ? 'Restore Note' : 'Clear Note'}
onClick={() => (history ? restore() : clear())}
/>
</div>
</header>
<textarea
className={styles.textarea}
dir="auto"
placeholder="What is on your mind?"
value={note}
onChange={e => write(e.target.value)}
/>
<p className={styles.counter}>
{characters} character{characters !== 1 && 's'} {words} word
{words !== 1 && 's'}
</p>
</Modal>
);
}

View File

@@ -0,0 +1,23 @@
.button {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: var(--font-sm);
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
outline: none;
transition: 0.2s;
&:hover {
background-color: var(--color-neutral-200);
}
&.smallIcon {
font-size: var(--font-xsm);
}
}

View File

@@ -0,0 +1,25 @@
import { Tooltip } from '@/components/tooltip';
import { cn } from '@/helpers/styles';
import styles from './button.module.css';
interface ButtonProps {
icon: React.ReactElement;
onClick: () => void;
smallIcon?: boolean;
tooltip: string;
}
export function Button({ icon, onClick, smallIcon, tooltip }: ButtonProps) {
return (
<Tooltip content={tooltip} hideDelay={0} placement="bottom" showDelay={0}>
<button
className={cn(styles.button, smallIcon && styles.smallIcon)}
onClick={onClick}
>
{icon}
</button>
</Tooltip>
);
}

View File

@@ -0,0 +1 @@
export { Button } from './button';

View File

@@ -0,0 +1 @@
export { Pomodoro } from './pomodoro';

View File

@@ -0,0 +1,36 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
& .title {
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground-subtle);
}
& .buttons {
display: flex;
column-gap: 4px;
align-items: center;
}
}
.control {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
& .completed {
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
}
& .buttons {
display: flex;
column-gap: 4px;
align-items: center;
}
}

View File

@@ -0,0 +1,168 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
import { IoMdSettings } from 'react-icons/io/index';
import { Modal } from '@/components/modal';
import { Tabs } from './tabs';
import { Timer } from './timer';
import { Button } from './button';
import { Setting } from './setting';
import { useLocalStorage } from '@/hooks/use-local-storage';
import { useSoundEffect } from '@/hooks/use-sound-effect';
import { usePomodoroStore } from '@/store';
import styles from './pomodoro.module.css';
interface PomodoroProps {
onClose: () => void;
show: boolean;
}
export function Pomodoro({ onClose, show }: PomodoroProps) {
const [showSetting, setShowSetting] = useState(false);
const [selectedTab, setSelectedTab] = useState('pomodoro');
const running = usePomodoroStore(state => state.running);
const setRunning = usePomodoroStore(state => state.setRunning);
const [timer, setTimer] = useState(0);
const interval = useRef<ReturnType<typeof setInterval> | null>(null);
const alarm = useSoundEffect('/sounds/alarm.mp3');
const defaultTimes = useMemo(
() => ({
long: 15 * 60,
pomodoro: 25 * 60,
short: 5 * 60,
}),
[],
);
const [times, setTimes] = useLocalStorage<Record<string, number>>(
'moodist-pomodoro-setting',
defaultTimes,
);
const [completions, setCompletions] = useState<Record<string, number>>({
long: 0,
pomodoro: 0,
short: 0,
});
const tabs = useMemo(
() => [
{ id: 'pomodoro', label: 'Pomodoro' },
{ id: 'short', label: 'Break' },
{ id: 'long', label: 'Long Break' },
],
[],
);
useEffect(() => {
if (running) {
if (interval.current) clearInterval(interval.current);
interval.current = setInterval(() => {
setTimer(prev => prev - 1);
}, 1000);
} else {
if (interval.current) clearInterval(interval.current);
}
}, [running]);
useEffect(() => {
if (timer <= 0 && running) {
if (interval.current) clearInterval(interval.current);
alarm.play();
setRunning(false);
setCompletions(prev => ({
...prev,
[selectedTab]: prev[selectedTab] + 1,
}));
}
}, [timer, selectedTab, running, setRunning, alarm]);
useEffect(() => {
const time = times[selectedTab] || 10;
if (interval.current) clearInterval(interval.current);
setRunning(false);
setTimer(time);
}, [selectedTab, times, setRunning]);
const toggleRunning = () => {
if (running) setRunning(false);
else if (timer <= 0) {
const time = times[selectedTab] || 10;
setTimer(time);
setRunning(true);
} else setRunning(true);
};
const restart = () => {
if (interval.current) clearInterval(interval.current);
const time = times[selectedTab] || 10;
setRunning(false);
setTimer(time);
};
return (
<>
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Pomodoro Timer</h2>
<div className={styles.button}>
<Button
icon={<IoMdSettings />}
tooltip="Change Times"
onClick={() => setShowSetting(true)}
/>
</div>
</header>
<Tabs selectedTab={selectedTab} tabs={tabs} onSelect={setSelectedTab} />
<Timer timer={timer} />
<div className={styles.control}>
<p className={styles.completed}>
{completions[selectedTab] || 0} completed
</p>
<div className={styles.buttons}>
<Button
icon={<FaUndo />}
smallIcon
tooltip="Restart"
onClick={restart}
/>
<Button
icon={running ? <FaPause /> : <FaPlay />}
smallIcon
tooltip={running ? 'Pause' : 'Start'}
onClick={toggleRunning}
/>
</div>
</div>
</Modal>
<Setting
show={showSetting}
times={times}
onClose={() => setShowSetting(false)}
onChange={times => {
setShowSetting(false);
setTimes(times);
}}
/>
</>
);
}

View File

@@ -0,0 +1 @@
export { Setting } from './setting';

View File

@@ -0,0 +1,66 @@
.title {
margin-bottom: 16px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .form {
display: flex;
flex-direction: column;
& .field {
display: flex;
flex-direction: column;
row-gap: 8px;
margin-bottom: 16px;
& .label {
font-size: var(--font-sm);
color: var(--color-foreground);
& span {
color: var(--color-foreground-subtle);
}
}
& .input {
display: block;
height: 40px;
padding: 0 8px;
color: var(--color-foreground);
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
outline: none;
}
}
& .buttons {
display: flex;
column-gap: 8px;
align-items: center;
justify-content: flex-end;
& 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-color: var(--color-neutral-200);
border: none;
border-radius: 4px;
outline: none;
&.primary {
color: var(--color-neutral-100);
background-color: var(--color-neutral-950);
}
}
}
}

View File

@@ -0,0 +1,92 @@
import { useEffect, useState } from 'react';
import { Modal } from '@/components/modal';
import styles from './setting.module.css';
interface SettingProps {
onChange: (newTimes: Record<string, number>) => void;
onClose: () => void;
show: boolean;
times: Record<string, number>;
}
export function Setting({ onChange, onClose, show, times }: SettingProps) {
const [values, setValues] = useState(times);
useEffect(() => setValues(times), [times]);
const handleChange = (id: string) => (value: number) => {
setValues(prev => ({ ...prev, [id]: value * 60 }));
};
const handleSubmit = e => {
e.preventDefault();
onChange(values);
};
return (
<Modal lockBody={false} show={show} onClose={onClose}>
<h2 className={styles.title}>Change Times</h2>
<form className={styles.form} onSubmit={handleSubmit}>
<Field
id="pomodoro"
label="Pomodoro"
value={values.pomodoro / 60}
onChange={handleChange('pomodoro')}
/>
<Field
id="short"
label="Short Break"
value={values.short / 60}
onChange={handleChange('short')}
/>
<Field
id="long"
label="Long Break"
value={values.long / 60}
onChange={handleChange('long')}
/>
<div className={styles.buttons}>
<button
onClick={e => {
e.preventDefault();
onClose();
}}
>
Cancel
</button>
<button className={styles.primary}>Save</button>
</div>
</form>
</Modal>
);
}
interface FieldProps {
id: string;
label: string;
onChange: (value: number) => void;
value: number;
}
function Field({ id, label, onChange, value }: FieldProps) {
return (
<div className={styles.field}>
<label className={styles.label} htmlFor={id}>
{label} <span>(minutes)</span>
</label>
<input
className={styles.input}
max={120}
min={1}
type="number"
value={value}
onChange={e => onChange(Number(e.target.value))}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export { Tabs } from './tabs';

View File

@@ -0,0 +1,36 @@
.tabs {
display: flex;
column-gap: 4px;
align-items: center;
padding: 4px;
margin: 8px 0;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
& .tab {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
height: 45px;
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
cursor: pointer;
background-color: transparent;
border: 1px solid transparent;
border-radius: 4px;
transition: 0.2s;
&.selected {
color: var(--color-foreground);
background-color: var(--color-neutral-200);
border-color: var(--color-neutral-300);
}
&:not(.selected):hover {
color: var(--color-foreground);
background-color: var(--color-neutral-100);
}
}
}

View File

@@ -0,0 +1,25 @@
import { cn } from '@/helpers/styles';
import styles from './tabs.module.css';
interface TabsProps {
onSelect: (id: string) => void;
selectedTab: string;
tabs: Array<{ id: string; label: string }>;
}
export function Tabs({ onSelect, selectedTab, tabs }: TabsProps) {
return (
<div className={styles.tabs}>
{tabs.map(tab => (
<button
className={cn(styles.tab, selectedTab === tab.id && styles.selected)}
key={tab.id}
onClick={() => onSelect(tab.id)}
>
{tab.label}
</button>
))}
</div>
);
}

View File

@@ -0,0 +1 @@
export { Timer } from './timer';

View File

@@ -0,0 +1,12 @@
.timer {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 30px 0;
font-size: var(--font-xlg);
font-weight: 500;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
}

View File

@@ -0,0 +1,15 @@
import { padNumber } from '@/helpers/number';
import styles from './timer.module.css';
interface TimerProps {
timer: number;
}
export function Timer({ timer }: TimerProps) {
return (
<div className={styles.timer}>
{padNumber(Math.floor(timer / 60))}:{padNumber(timer % 60)}
</div>
);
}

11
src/helpers/counter.ts Normal file
View File

@@ -0,0 +1,11 @@
export function count(_string: string) {
const string = _string.trim();
return {
characters: string.replace(/\s/g, '').length,
words: string
.replace(/\n/g, ' ')
.split(' ')
.filter(str => str !== '').length,
};
}

9
src/helpers/download.ts Normal file
View File

@@ -0,0 +1,9 @@
export function download(filename: string, content: string) {
const element = document.createElement('a');
element.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
);
element.setAttribute('download', filename);
element.click();
}

3
src/helpers/number.ts Normal file
View File

@@ -0,0 +1,3 @@
export function padNumber(number: number, maxLength: number = 2): string {
return number.toString().padStart(maxLength, '0');
}

View File

@@ -0,0 +1,45 @@
import { useMemo, useEffect, useCallback } from 'react';
import { Howl } from 'howler';
import { useSSR } from './use-ssr';
export function useSoundEffect(src: string, volume: number = 1) {
const { isBrowser } = useSSR();
const sound = useMemo<Howl | null>(() => {
let sound: Howl | null = null;
if (isBrowser) {
sound = new Howl({
html5: true,
src: src,
});
}
return sound;
}, [src, isBrowser]);
useEffect(() => {
if (sound) sound.volume(typeof volume === 'number' ? volume : 1);
}, [sound, volume]);
const play = useCallback(() => {
if (sound) {
if (!sound.playing()) {
sound.play();
}
}
}, [sound]);
const stop = useCallback(() => {
if (sound) sound.stop();
}, [sound]);
const pause = useCallback(() => {
if (sound) sound.pause();
}, [sound]);
const control = useMemo(() => ({ pause, play, stop }), [play, stop, pause]);
return control;
}

View File

@@ -1,4 +1,6 @@
---
import { count } from '@/lib/sounds';
import '@/styles/global.css';
interface Props {
@@ -9,14 +11,13 @@ interface Props {
const title = Astro.props.title || 'Moodist: Ambient Sounds for Focus and Calm';
const description =
Astro.props.description ||
"Moodist is a free online ambient sound generator for focus and calm, offering 35 handpicked sounds in various categories, from nature's tranquil melodies to the soothing ambiance of urban life.";
`Moodist is a free and open-source ambient sound generator featuring ${count()} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.`;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="Astro description" name="description" />
<meta content="width=device-width" name="viewport" />
<meta content={Astro.generator} name="generator" />
<meta content="#18181b" name="theme-color" />

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