mirror of
https://github.com/remvze/moodist.git
synced 2026-03-06 03:53:13 +08:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ff67c9e6 | ||
|
|
1f806c4e56 | ||
|
|
e6f768a5e6 | ||
|
|
8e0291004a | ||
|
|
d449c29321 | ||
|
|
f12ca4806c | ||
|
|
17b4f25ff1 | ||
|
|
f877e49763 | ||
|
|
f1d212abc8 | ||
|
|
38f6f7dbe6 | ||
|
|
937bf29d09 | ||
|
|
e2172fd2bb | ||
|
|
1f12afa394 | ||
|
|
d96461d1ea | ||
|
|
5467bbbc24 | ||
|
|
32da26ccfc | ||
|
|
81f33d9d37 | ||
|
|
463667c868 | ||
|
|
b77c817db2 | ||
|
|
216b913ccd | ||
|
|
e422b52436 | ||
|
|
1f635348e3 | ||
|
|
889962babe | ||
|
|
5e0a84259f | ||
|
|
8e4d0531e0 | ||
|
|
cd05704a73 | ||
|
|
01b4bdbb57 | ||
|
|
f682a910da | ||
|
|
a33ae450cf | ||
|
|
b8bc9c8b4c | ||
|
|
ee606139a8 | ||
|
|
7823dc7ff4 | ||
|
|
37a0736a0e | ||
|
|
c51acd6261 | ||
|
|
ff26597d22 | ||
|
|
c8e51226e5 | ||
|
|
131ab29621 | ||
|
|
0f62f0795c | ||
|
|
1a23e004a6 | ||
|
|
93ff72a052 | ||
|
|
ef81f198ba | ||
|
|
35e32152b1 | ||
|
|
26bf01690c | ||
|
|
fe2357c995 | ||
|
|
85b627ecb9 | ||
|
|
8beb42cb1b | ||
|
|
17027e299b | ||
|
|
184bb09f5a | ||
|
|
660ee07a23 | ||
|
|
cb4bfea5ab | ||
|
|
210bd234e0 | ||
|
|
64ef5c5138 | ||
|
|
1218751a6f | ||
|
|
a234bc17a6 |
@@ -45,6 +45,7 @@
|
||||
"sort-keys-fix/sort-keys-fix": ["warn", "asc"],
|
||||
"sort-destructure-keys/sort-destructure-keys": "warn",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"react/jsx-sort-props": [
|
||||
"warn",
|
||||
{
|
||||
|
||||
34
.github/workflows/build_docker.yml
vendored
Normal file
34
.github/workflows/build_docker.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Build and push main image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push-store-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: 'Login to GitHub Container Registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{secrets.ACCESS_TOKEN}}
|
||||
|
||||
- name: 'Build 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
|
||||
102
CHANGELOG.md
102
CHANGELOG.md
@@ -2,6 +2,108 @@
|
||||
|
||||
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.3.1](https://github.com/remvze/moodist/compare/v1.3.0...v1.3.1) (2024-02-01)
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* add donate item ([f12ca48](https://github.com/remvze/moodist/commit/f12ca4806c9279f69f298bef770f8cac69a0860a))
|
||||
* add donate section ([d449c29](https://github.com/remvze/moodist/commit/d449c29321024a43517e92cc59223b4b22fe2e82))
|
||||
* add donation header ([17b4f25](https://github.com/remvze/moodist/commit/17b4f25ff10e09a917203e67cf963cac8358de1a))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* coffee typo ([8e02910](https://github.com/remvze/moodist/commit/8e0291004a90e55b67a921b9ffb483b409109ae4))
|
||||
* complete donation links ([e6f768a](https://github.com/remvze/moodist/commit/e6f768a5e6dc983ae04b70f6c434fd4c13aeb506))
|
||||
|
||||
|
||||
### 🚚 Chores
|
||||
|
||||
* add donation link to README file ([1f806c4](https://github.com/remvze/moodist/commit/1f806c4e561d79a00850130eda09376299d85ed2))
|
||||
|
||||
## [1.3.0](https://github.com/remvze/moodist/compare/v1.2.0...v1.3.0) (2024-02-01)
|
||||
|
||||
|
||||
### ♻️ Code Refactoring
|
||||
|
||||
* remove media session ([1f63534](https://github.com/remvze/moodist/commit/1f635348e3e5cf73ee76e1c5fac7b5f5b7f7ea6a))
|
||||
* remove unmute and media session ([b77c817](https://github.com/remvze/moodist/commit/b77c817db25e1a738b6770b1ae86d792e0d42240))
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* add fading to intro and outro ([5467bbb](https://github.com/remvze/moodist/commit/5467bbbc2437a5504e157122a995ad7a565ff0b8))
|
||||
* add loader for favorites ([f682a91](https://github.com/remvze/moodist/commit/f682a910da97eb53cfb90ce955e953f05088e686))
|
||||
* add media session ([5e0a842](https://github.com/remvze/moodist/commit/5e0a84259ff5586700c4e10087485d905be7ccee))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* add audio element ([889962b](https://github.com/remvze/moodist/commit/889962babe6e940ff283a41b145620d2a0477c70))
|
||||
* add media session ([81f33d9](https://github.com/remvze/moodist/commit/81f33d9d375f63b4dd0bf58ad28a72354d85706e))
|
||||
* add media session ([216b913](https://github.com/remvze/moodist/commit/216b913ccd0a7dfe0d03575f842aac9711ef0216))
|
||||
* add unmute for iOS ([e422b52](https://github.com/remvze/moodist/commit/e422b52436c7dfc0b6cf866afa2b74dc219dcf2f))
|
||||
* connect audio context to audio element ([463667c](https://github.com/remvze/moodist/commit/463667c868371540c46c9007e686961f9a4be7e5))
|
||||
* increase decimal ([a33ae45](https://github.com/remvze/moodist/commit/a33ae450cf2c883228c76d04df8df75839c12753))
|
||||
* remove fading ([d96461d](https://github.com/remvze/moodist/commit/d96461d1ea83c72bfe651d84cf34fabc029c200e))
|
||||
* resume audio ([8e4d053](https://github.com/remvze/moodist/commit/8e4d0531e0e9aaf4e52b3b3a8666b74ff0c0222e))
|
||||
* undo changes ([32da26c](https://github.com/remvze/moodist/commit/32da26ccfc0c5bdbe031e26ea48363ea0d8a7b23))
|
||||
|
||||
|
||||
### 🚚 Chores
|
||||
|
||||
* add binaural beats ([f1d212a](https://github.com/remvze/moodist/commit/f1d212abc8b69a614bbdc4a23876e2eab7cbb574))
|
||||
* add more sounds ([38f6f7d](https://github.com/remvze/moodist/commit/38f6f7dbe6898ed78e51eb3f0c7936f003ddca08))
|
||||
* add more sounds ([937bf29](https://github.com/remvze/moodist/commit/937bf29d09cbce20ea0b6b0c87879f3a7dd1d497))
|
||||
* add more sounds ([e2172fd](https://github.com/remvze/moodist/commit/e2172fd2bbd0e12a705c9efc98c72ad99d86d006))
|
||||
* add more sounds ([1f12afa](https://github.com/remvze/moodist/commit/1f12afa3943154d70145ef6adc6aeee79f7a7af3))
|
||||
* add more sounds ([cd05704](https://github.com/remvze/moodist/commit/cd05704a73ffb33aa0ccf5d789328a4cefc320f1))
|
||||
* add more sounds ([01b4bdb](https://github.com/remvze/moodist/commit/01b4bdbb572285984bcdc9bb94c1a1b6dd2630c5))
|
||||
|
||||
## [1.2.0](https://github.com/remvze/moodist/compare/v1.1.0...v1.2.0) (2024-01-04)
|
||||
|
||||
|
||||
### ♻️ Code Refactoring
|
||||
|
||||
* better item structure for menu ([26bf016](https://github.com/remvze/moodist/commit/26bf01690cfcc105b661951bcb2347394a67fb68))
|
||||
* rewrite menu with floating ui ([8beb42c](https://github.com/remvze/moodist/commit/8beb42cb1b92c99aa9656b35cd7d82094e5baf72))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* stringify dependency ([1a23e00](https://github.com/remvze/moodist/commit/1a23e004a65960ce169990211f150db25762fead))
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* add animation to menu box ([17027e2](https://github.com/remvze/moodist/commit/17027e299bb9bf958aebaf735c40e7664ad71e8b))
|
||||
* add disabled state ([ff26597](https://github.com/remvze/moodist/commit/ff26597d22d444d18d2874a5c278eccc288972de))
|
||||
* add menu button ([184bb09](https://github.com/remvze/moodist/commit/184bb09f5ab09fcf877e6a904023d9de72be9a89))
|
||||
* add share modal ([35e3215](https://github.com/remvze/moodist/commit/35e32152b153f4dfaf9e071f526f6d7602ea97fc))
|
||||
* add share placeholder ([fe2357c](https://github.com/remvze/moodist/commit/fe2357c995713cd0fb8335b325266859dc47a769))
|
||||
* basic structure for share link ([ef81f19](https://github.com/remvze/moodist/commit/ef81f198baeb927e3b1768570f75e6638a7bd0b6))
|
||||
* **docker:** add dockerfile ([a234bc1](https://github.com/remvze/moodist/commit/a234bc17a66331acbbc1d980cd1f53d58646f534))
|
||||
* implement override feature ([0f62f07](https://github.com/remvze/moodist/commit/0f62f0795c5a9e06fa4e62b6b7b1e6c0774dfe0f))
|
||||
* implement sharing URL ([93ff72a](https://github.com/remvze/moodist/commit/93ff72a052484b36c9ac821b94b632865b4a3550))
|
||||
|
||||
|
||||
### 💄 Styling
|
||||
|
||||
* add animation to modal ([7823dc7](https://github.com/remvze/moodist/commit/7823dc7ff473278ef8ee401e69796c17b33da794))
|
||||
* add icon to menu items ([131ab29](https://github.com/remvze/moodist/commit/131ab296215812e45a0c60486d75683f3de25d16))
|
||||
* change border color ([85b627e](https://github.com/remvze/moodist/commit/85b627ecb96a4f52ecacdb53ed4484c050adba5e))
|
||||
* change copy ([c51acd6](https://github.com/remvze/moodist/commit/c51acd62618cc705902dc01f0574a2c9124264c5))
|
||||
* change to primary color ([c8e5122](https://github.com/remvze/moodist/commit/c8e51226e57bfa72ad91318de25fc5f9b5751634))
|
||||
* widen the menu ([37a0736](https://github.com/remvze/moodist/commit/37a0736a0e7edd09c33940099c884e5b48afbbf1))
|
||||
|
||||
|
||||
### 🚚 Chores
|
||||
|
||||
* change docker workflow ([cb4bfea](https://github.com/remvze/moodist/commit/cb4bfea5ab4326dee17c78554f12a08ffcb9dd0e))
|
||||
* change docker-compose file ([660ee07](https://github.com/remvze/moodist/commit/660ee07a2359ec77c9d56bbe552541246e0f79c5))
|
||||
* update GitHub action ([ee60613](https://github.com/remvze/moodist/commit/ee606139a80121fd6ee1b8233f82af994c4e1178))
|
||||
|
||||
## [1.1.0](https://github.com/remvze/moodist/compare/v1.0.0...v1.1.0) (2023-12-29)
|
||||
|
||||
|
||||
|
||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine3.18 AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine AS runtime
|
||||
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8080
|
||||
@@ -2,5 +2,5 @@
|
||||
<img src="/assets/banner.svg" 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://moodist.app">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
|
||||
</div>
|
||||
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: '3.9'
|
||||
services:
|
||||
moodist:
|
||||
image: ghcr.io/remvze/moodist
|
||||
logging:
|
||||
options:
|
||||
max-size: 1g
|
||||
restart: always
|
||||
ports:
|
||||
- '8080:8080'
|
||||
31
docker/nginx/nginx.conf
Normal file
31
docker/nginx/nginx.conf
Normal file
@@ -0,0 +1,31 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/index.html =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "moodist",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "moodist",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.1",
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@floating-ui/react": "0.26.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "moodist",
|
||||
"type": "module",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
|
||||
BIN
public/fonts/inter-tight-v7-latin-700.woff2
Normal file
BIN
public/fonts/inter-tight-v7-latin-700.woff2
Normal file
Binary file not shown.
BIN
public/sounds/animals/crows.mp3
Normal file
BIN
public/sounds/animals/crows.mp3
Normal file
Binary file not shown.
BIN
public/sounds/animals/whale.mp3
Normal file
BIN
public/sounds/animals/whale.mp3
Normal file
Binary file not shown.
BIN
public/sounds/binaural/binaural-alpha.wav
Normal file
BIN
public/sounds/binaural/binaural-alpha.wav
Normal file
Binary file not shown.
BIN
public/sounds/binaural/binaural-beta.wav
Normal file
BIN
public/sounds/binaural/binaural-beta.wav
Normal file
Binary file not shown.
BIN
public/sounds/binaural/binaural-delta.wav
Normal file
BIN
public/sounds/binaural/binaural-delta.wav
Normal file
Binary file not shown.
BIN
public/sounds/binaural/binaural-gamma.wav
Normal file
BIN
public/sounds/binaural/binaural-gamma.wav
Normal file
Binary file not shown.
BIN
public/sounds/binaural/binaural-theta.wav
Normal file
BIN
public/sounds/binaural/binaural-theta.wav
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/sounds/places/carousel.mp3
Normal file
BIN
public/sounds/places/carousel.mp3
Normal file
Binary file not shown.
BIN
public/sounds/places/crowded-bar.mp3
Normal file
BIN
public/sounds/places/crowded-bar.mp3
Normal file
Binary file not shown.
BIN
public/sounds/places/laboratory.mp3
Normal file
BIN
public/sounds/places/laboratory.mp3
Normal file
Binary file not shown.
BIN
public/sounds/places/night-village.mp3
Normal file
BIN
public/sounds/places/night-village.mp3
Normal file
Binary file not shown.
BIN
public/sounds/places/office.mp3
Normal file
BIN
public/sounds/places/office.mp3
Normal file
Binary file not shown.
BIN
public/sounds/places/subway-station.mp3
Normal file
BIN
public/sounds/places/subway-station.mp3
Normal file
Binary file not shown.
BIN
public/sounds/places/supermarket.mp3
Normal file
BIN
public/sounds/places/supermarket.mp3
Normal file
Binary file not shown.
BIN
public/sounds/things/boiling-water.mp3
Normal file
BIN
public/sounds/things/boiling-water.mp3
Normal file
Binary file not shown.
BIN
public/sounds/things/bubbles.mp3
Normal file
BIN
public/sounds/things/bubbles.mp3
Normal file
Binary file not shown.
BIN
public/sounds/things/dryer.mp3
Normal file
BIN
public/sounds/things/dryer.mp3
Normal file
Binary file not shown.
BIN
public/sounds/things/morse-code.mp3
Normal file
BIN
public/sounds/things/morse-code.mp3
Normal file
Binary file not shown.
BIN
public/sounds/things/slide-projector.mp3
Normal file
BIN
public/sounds/things/slide-projector.mp3
Normal file
Binary file not shown.
BIN
public/sounds/things/tuning-radio.mp3
Normal file
BIN
public/sounds/things/tuning-radio.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/sounds/urban/fireworks.mp3
Normal file
BIN
public/sounds/urban/fireworks.mp3
Normal file
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { BiSolidHeart } from 'react-icons/bi/index';
|
||||
import { Howler } from 'howler';
|
||||
|
||||
import { useSoundStore } from '@/store';
|
||||
|
||||
@@ -9,7 +10,8 @@ import { StoreConsumer } from '@/components/store-consumer';
|
||||
import { Buttons } from '@/components/buttons';
|
||||
import { Categories } from '@/components/categories';
|
||||
import { ScrollToTop } from '@/components/scroll-to-top';
|
||||
import { Shuffle } from '@/components/shuffle';
|
||||
import { SharedModal } from '@/components/modals/shared';
|
||||
import { Menu } from '@/components/menu/menu';
|
||||
import { SnackbarProvider } from '@/contexts/snackbar';
|
||||
|
||||
import { sounds } from '@/data/sounds';
|
||||
@@ -35,6 +37,22 @@ export function App() {
|
||||
);
|
||||
}, [favorites, categories]);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
const { ctx } = Howler;
|
||||
|
||||
if (ctx && !document.hidden) {
|
||||
setTimeout(() => {
|
||||
ctx.resume();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', onChange, false);
|
||||
|
||||
return () => document.removeEventListener('visibilitychange', onChange);
|
||||
}, []);
|
||||
|
||||
const allCategories = useMemo(() => {
|
||||
const favorites = [];
|
||||
|
||||
@@ -60,7 +78,8 @@ export function App() {
|
||||
</Container>
|
||||
|
||||
<ScrollToTop />
|
||||
<Shuffle />
|
||||
<Menu />
|
||||
<SharedModal />
|
||||
</StoreConsumer>
|
||||
</SnackbarProvider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
import { Category } from '@/components/category';
|
||||
import { Donate } from './donate';
|
||||
|
||||
import type { Categories } from '@/data/types';
|
||||
|
||||
@@ -11,12 +12,16 @@ interface CategoriesProps {
|
||||
export function Categories({ categories }: CategoriesProps) {
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{categories.map(category => (
|
||||
<Category
|
||||
functional={category.id !== 'favorites'}
|
||||
{...category}
|
||||
key={category.id}
|
||||
/>
|
||||
{categories.map((category, index) => (
|
||||
<>
|
||||
<Category
|
||||
functional={category.id !== 'favorites'}
|
||||
{...category}
|
||||
key={category.id}
|
||||
/>
|
||||
|
||||
{index === 3 && <Donate />}
|
||||
</>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
63
src/components/categories/donate/donate.module.css
Normal file
63
src/components/categories/donate/donate.module.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.donate {
|
||||
& .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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/components/categories/donate/donate.tsx
Normal file
27
src/components/categories/donate/donate.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { FaCoffee } from 'react-icons/fa/index';
|
||||
|
||||
import styles from './donate.module.css';
|
||||
|
||||
export function Donate() {
|
||||
return (
|
||||
<div className={styles.donate}>
|
||||
<div className={styles.iconContainer}>
|
||||
<div className={styles.tail} />
|
||||
<div className={styles.icon}>
|
||||
<FaCoffee />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className={styles.title}>Support Me</h2>
|
||||
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
|
||||
<a
|
||||
className={styles.button}
|
||||
href="https://buymeacoffee.com/remvze"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Donate Today
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/components/categories/donate/index.ts
Normal file
1
src/components/categories/donate/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Donate } from './donate';
|
||||
56
src/components/donate.astro
Normal file
56
src/components/donate.astro
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
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>
|
||||
@@ -37,7 +37,7 @@ const count = soundCount();
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: 140px 0 60px;
|
||||
padding: 100px 0 60px;
|
||||
text-align: center;
|
||||
|
||||
& .logo {
|
||||
|
||||
1
src/components/menu/index.ts
Normal file
1
src/components/menu/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Menu } from './menu';
|
||||
1
src/components/menu/item/index.ts
Normal file
1
src/components/menu/item/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Item } from './item';
|
||||
34
src/components/menu/item/item.module.css
Normal file
34
src/components/menu/item/item.module.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.item {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
padding: 16px 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-radius: 4px;
|
||||
outline: none;
|
||||
transition: 0.2s;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-200);
|
||||
}
|
||||
|
||||
& .icon {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
}
|
||||
30
src/components/menu/item/item.tsx
Normal file
30
src/components/menu/item/item.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import styles from './item.module.css';
|
||||
|
||||
interface ItemProps {
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
icon: React.ReactElement;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Item({
|
||||
disabled = false,
|
||||
href,
|
||||
icon,
|
||||
label,
|
||||
onClick = () => {},
|
||||
}: ItemProps) {
|
||||
const Comp = href ? 'a' : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={styles.item}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
{...(href ? { href, target: '_blank' } : {})}
|
||||
>
|
||||
<span className={styles.icon}>{icon}</span> {label}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
13
src/components/menu/items/donate.tsx
Normal file
13
src/components/menu/items/donate.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SiBuymeacoffee } from 'react-icons/si/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Donate() {
|
||||
return (
|
||||
<Item
|
||||
href="https://buymeacoffee.com/remvze"
|
||||
icon={<SiBuymeacoffee />}
|
||||
label="Buy Me a Coffee"
|
||||
/>
|
||||
);
|
||||
}
|
||||
3
src/components/menu/items/index.ts
Normal file
3
src/components/menu/items/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Shuffle as ShuffleItem } from './shuffle';
|
||||
export { Share as ShareItem } from './share';
|
||||
export { Donate as DonateItem } from './donate';
|
||||
22
src/components/menu/items/share.tsx
Normal file
22
src/components/menu/items/share.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IoShareSocialSharp } from 'react-icons/io5/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
import { useSoundStore } from '@/store';
|
||||
|
||||
interface ShareProps {
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
export function Share({ open }: ShareProps) {
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
||||
return (
|
||||
<Item
|
||||
disabled={noSelected}
|
||||
icon={<IoShareSocialSharp />}
|
||||
label="Share Sounds"
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
11
src/components/menu/items/shuffle.tsx
Normal file
11
src/components/menu/items/shuffle.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BiShuffle } from 'react-icons/bi/index';
|
||||
|
||||
import { useSoundStore } from '@/store';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Shuffle() {
|
||||
const shuffle = useSoundStore(state => state.shuffle);
|
||||
|
||||
return <Item icon={<BiShuffle />} label="Shuffle Sounds" onClick={shuffle} />;
|
||||
}
|
||||
57
src/components/menu/menu.module.css
Normal file
57
src/components/menu/menu.module.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.wrapper {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 5;
|
||||
|
||||
& .menuButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: var(--font-md);
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-neutral-300);
|
||||
border-radius: 50%;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-neutral-200);
|
||||
}
|
||||
}
|
||||
|
||||
& .menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
width: 240px;
|
||||
padding: 4px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/components/menu/menu.tsx
Normal file
93
src/components/menu/menu.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { IoMenu, IoClose } from 'react-icons/io5/index';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
useFloating,
|
||||
autoUpdate,
|
||||
offset,
|
||||
flip,
|
||||
shift,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useRole,
|
||||
useInteractions,
|
||||
FloatingFocusManager,
|
||||
} from '@floating-ui/react';
|
||||
|
||||
import { ShuffleItem, ShareItem, DonateItem } from './items';
|
||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
||||
|
||||
import { slideY, fade, mix } from '@/lib/motion';
|
||||
|
||||
import styles from './menu.module.css';
|
||||
|
||||
export function Menu() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [showShareLink, setShowShareLink] = useState(false);
|
||||
|
||||
const variants = mix(slideY(-20), fade());
|
||||
|
||||
const { context, floatingStyles, refs } = useFloating({
|
||||
middleware: [offset(12), flip(), shift()],
|
||||
onOpenChange: setIsOpen,
|
||||
open: isOpen,
|
||||
placement: 'top-end',
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const click = useClick(context);
|
||||
const dismiss = useDismiss(context);
|
||||
const role = useRole(context);
|
||||
|
||||
const { getFloatingProps, getReferenceProps } = useInteractions([
|
||||
click,
|
||||
dismiss,
|
||||
role,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
<button
|
||||
aria-label="Menu"
|
||||
className={styles.menuButton}
|
||||
ref={refs.setReference}
|
||||
onClick={() => setIsOpen(prev => !prev)}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
|
||||
<ShareLinkModal
|
||||
show={showShareLink}
|
||||
onClose={() => setShowShareLink(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
src/components/modal/index.ts
Normal file
1
src/components/modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Modal } from './modal';
|
||||
49
src/components/modal/modal.module.css
Normal file
49
src/components/modal/modal.module.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
background-color: rgb(9 9 11 / 40%);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 12;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
padding: 50px 0;
|
||||
overflow-y: auto;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
& .content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
padding: 20px;
|
||||
padding-top: 40px;
|
||||
margin: 0 auto;
|
||||
pointer-events: fill;
|
||||
background-color: var(--color-neutral-100);
|
||||
border-radius: 8px;
|
||||
|
||||
& .close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
color: var(--color-foreground-subtle);
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/components/modal/modal.tsx
Normal file
52
src/components/modal/modal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { IoClose } from 'react-icons/io5/index';
|
||||
|
||||
import { fade, mix, slideY } from '@/lib/motion';
|
||||
|
||||
import styles from './modal.module.css';
|
||||
|
||||
interface ModalProps {
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export function Modal({ children, onClose, show }: ModalProps) {
|
||||
const variants = {
|
||||
modal: mix(fade(), slideY(20)),
|
||||
overlay: fade(),
|
||||
};
|
||||
|
||||
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}>
|
||||
<motion.div
|
||||
animate="show"
|
||||
className={styles.content}
|
||||
exit="hidden"
|
||||
initial="hidden"
|
||||
variants={variants.modal}
|
||||
>
|
||||
<button className={styles.close} onClick={onClose}>
|
||||
<IoClose />
|
||||
</button>
|
||||
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
1
src/components/modals/share-link/index.ts
Normal file
1
src/components/modals/share-link/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ShareLinkModal } from './share-link';
|
||||
53
src/components/modals/share-link/share-link.module.css
Normal file
53
src/components/modals/share-link/share-link.module.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.heading {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
padding: 4px;
|
||||
margin-top: 12px;
|
||||
background-color: var(--color-neutral-50);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 4px;
|
||||
|
||||
& input {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
font-size: var(--font-sm);
|
||||
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;
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-100);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-neutral-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/components/modals/share-link/share-link.tsx
Normal file
67
src/components/modals/share-link/share-link.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
|
||||
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useCopy } from '@/hooks/use-copy';
|
||||
import { useSoundStore } from '@/store';
|
||||
|
||||
import styles from './share-link.module.css';
|
||||
|
||||
interface ShareLinkModalProps {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const sounds = useSoundStore(state => state.sounds);
|
||||
const { copy, copying } = useCopy();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
return Object.keys(sounds)
|
||||
.map(sound => ({
|
||||
id: sound,
|
||||
isSelected: sounds[sound].isSelected,
|
||||
volume: sounds[sound].volume.toFixed(2),
|
||||
}))
|
||||
.filter(sound => sound.isSelected);
|
||||
}, [sounds, JSON.stringify(sounds)]); // eslint-disable-line
|
||||
|
||||
const string = useMemo(() => {
|
||||
const object: Record<string, number> = {};
|
||||
|
||||
selected.forEach(sound => {
|
||||
object[sound.id] = Number(sound.volume);
|
||||
});
|
||||
|
||||
return JSON.stringify(object);
|
||||
}, [selected]);
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (!isMounted)
|
||||
return `https://moodist.app/?share=${encodeURIComponent(string)}`;
|
||||
|
||||
return `${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/?share=${encodeURIComponent(string)}`;
|
||||
}, [string, isMounted]);
|
||||
|
||||
useEffect(() => setIsMounted(true), []);
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h1 className={styles.heading}>Share your sound selection!</h1>
|
||||
<p className={styles.desc}>
|
||||
Copy and send the following link to the person you want to share your
|
||||
selection with.
|
||||
</p>
|
||||
<div className={styles.inputWrapper}>
|
||||
<input readOnly type="text" value={url} />
|
||||
<button onClick={() => copy(url)}>
|
||||
{copying ? <IoCheckmark /> : <IoCopyOutline />}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
1
src/components/modals/shared/index.ts
Normal file
1
src/components/modals/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SharedModal } from './shared';
|
||||
68
src/components/modals/shared/shared.module.css
Normal file
68
src/components/modals/shared/shared.module.css
Normal file
@@ -0,0 +1,68 @@
|
||||
.heading {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.sounds {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
background-color: var(--color-neutral-50);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 4px;
|
||||
|
||||
& .sound {
|
||||
padding: 8px 16px;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
background-color: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
|
||||
.button {
|
||||
padding: 12px 16px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-foreground-subtle);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-200);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--color-neutral-200);
|
||||
background-color: var(--color-neutral-950);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-neutral-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/components/modals/shared/shared.tsx
Normal file
107
src/components/modals/shared/shared.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useSoundStore } from '@/store';
|
||||
import { useSnackbar } from '@/contexts/snackbar';
|
||||
import { cn } from '@/helpers/styles';
|
||||
import { sounds } from '@/data/sounds';
|
||||
|
||||
import styles from './shared.module.css';
|
||||
|
||||
export function SharedModal() {
|
||||
const override = useSoundStore(state => state.override);
|
||||
const showSnackbar = useSnackbar();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [sharedSounds, setSharedSounds] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
volume: number;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const share = searchParams.get('share');
|
||||
|
||||
if (share) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(share));
|
||||
const allSounds: Record<string, string> = {};
|
||||
|
||||
sounds.categories.forEach(category => {
|
||||
category.sounds.forEach(sound => {
|
||||
allSounds[sound.id] = sound.label;
|
||||
});
|
||||
});
|
||||
|
||||
const _sharedSounds: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
volume: number;
|
||||
}> = [];
|
||||
|
||||
Object.keys(parsed).forEach(sound => {
|
||||
if (allSounds[sound]) {
|
||||
_sharedSounds.push({
|
||||
id: sound,
|
||||
label: allSounds[sound],
|
||||
volume: Number(parsed[sound]),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (_sharedSounds.length) {
|
||||
setIsOpen(true);
|
||||
setSharedSounds(_sharedSounds);
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
} finally {
|
||||
history.pushState({}, '', location.href.split('?')[0]);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOverride = () => {
|
||||
const newSounds: Record<string, number> = {};
|
||||
|
||||
sharedSounds.forEach(sound => {
|
||||
newSounds[sound.id] = sound.volume;
|
||||
});
|
||||
|
||||
override(newSounds);
|
||||
setIsOpen(false);
|
||||
showSnackbar('Done! You can now play the new selection.');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onClose={() => setIsOpen(false)}>
|
||||
<h1 className={styles.heading}>New sound mix detected!</h1>
|
||||
<p className={styles.desc}>
|
||||
Someone has shared the following mix with you. Would you want to
|
||||
override your current selection?
|
||||
</p>
|
||||
<div className={styles.sounds}>
|
||||
{sharedSounds.map(sound => (
|
||||
<div className={styles.sound} key={sound.id}>
|
||||
{sound.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={handleOverride}
|
||||
>
|
||||
Override
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Range } from './range';
|
||||
import { Favorite } from './favorite';
|
||||
|
||||
import { useSound } from '@/hooks/use-sound';
|
||||
import { useSoundStore } from '@/store';
|
||||
import { useSoundStore, useLoadingStore } from '@/store';
|
||||
import { cn } from '@/helpers/styles';
|
||||
|
||||
import styles from './sound.module.css';
|
||||
@@ -37,6 +37,8 @@ export function Sound({
|
||||
const volume = useSoundStore(state => state.sounds[id].volume);
|
||||
const isSelected = useSoundStore(state => state.sounds[id].isSelected);
|
||||
|
||||
const isLoading = useLoadingStore(state => state.loaders[src]);
|
||||
|
||||
const sound = useSound(src, { loop: true, volume });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,7 +82,7 @@ export function Sound({
|
||||
>
|
||||
<Favorite id={id} />
|
||||
<div className={styles.icon}>
|
||||
{sound.isLoading ? (
|
||||
{isLoading ? (
|
||||
<span className={styles.spinner}>
|
||||
<ImSpinner9 />
|
||||
</span>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
content: '';
|
||||
background-color: #34d399;
|
||||
background-color: var(--color-neutral-950);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,22 @@ import { places } from './sounds/places';
|
||||
import { transport } from './sounds/transport';
|
||||
import { things } from './sounds/things';
|
||||
import { noise } from './sounds/noise';
|
||||
import { binaural } from './sounds/binaural';
|
||||
|
||||
import type { Categories } from './types';
|
||||
|
||||
export const sounds: {
|
||||
categories: Categories;
|
||||
} = {
|
||||
categories: [nature, rain, animals, urban, places, transport, things, noise],
|
||||
categories: [
|
||||
nature,
|
||||
rain,
|
||||
animals,
|
||||
urban,
|
||||
places,
|
||||
transport,
|
||||
things,
|
||||
noise,
|
||||
binaural,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { GiCricket, GiSeagull, GiWolfHead, GiOwl } from 'react-icons/gi/index';
|
||||
import { FaDog, FaFrog, FaHorseHead, FaCat } from 'react-icons/fa/index';
|
||||
import {
|
||||
GiCricket,
|
||||
GiSeagull,
|
||||
GiWolfHead,
|
||||
GiOwl,
|
||||
GiWhaleTail,
|
||||
} from 'react-icons/gi/index';
|
||||
import {
|
||||
FaDog,
|
||||
FaFrog,
|
||||
FaHorseHead,
|
||||
FaCat,
|
||||
FaCrow,
|
||||
} from 'react-icons/fa/index';
|
||||
import { PiBirdFill, PiDogBold } from 'react-icons/pi/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
@@ -62,6 +74,18 @@ export const animals: Category = {
|
||||
label: 'Cat Purring',
|
||||
src: '/sounds/animals/cat-purring.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaCrow />,
|
||||
id: 'crows',
|
||||
label: 'Crows',
|
||||
src: '/sounds/animals/crows.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWhaleTail />,
|
||||
id: 'whale',
|
||||
label: 'Whale',
|
||||
src: '/sounds/animals/whale.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Animals',
|
||||
};
|
||||
|
||||
42
src/data/sounds/binaural.tsx
Normal file
42
src/data/sounds/binaural.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { TbWaveSine } from 'react-icons/tb/index';
|
||||
import { BsSoundwave } from 'react-icons/bs/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
|
||||
export const binaural: Category = {
|
||||
icon: <TbWaveSine />,
|
||||
id: 'binaural',
|
||||
sounds: [
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-delta',
|
||||
label: 'Delta',
|
||||
src: '/sounds/binaural/binaural-delta.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-theta',
|
||||
label: 'Theta',
|
||||
src: '/sounds/binaural/binaural-theta.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-alpha',
|
||||
label: 'Alpha',
|
||||
src: '/sounds/binaural/binaural-alpha.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-beta',
|
||||
label: 'Beta',
|
||||
src: '/sounds/binaural/binaural-beta.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-gamma',
|
||||
label: 'Gamma',
|
||||
src: '/sounds/binaural/binaural-gamma.wav',
|
||||
},
|
||||
],
|
||||
title: 'Binaural Beats',
|
||||
};
|
||||
@@ -40,7 +40,7 @@ export const nature: Category = {
|
||||
src: '/sounds/nature/howling-wind.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaWind />,
|
||||
icon: <BiSolidTree />,
|
||||
id: 'wind-in-trees',
|
||||
label: 'Wind in Trees',
|
||||
src: '/sounds/nature/wind-in-trees.mp3',
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { BiSolidCoffeeAlt, BiSolidPlaneAlt } from 'react-icons/bi/index';
|
||||
import { FaChurch } from 'react-icons/fa/index';
|
||||
import { TbScubaMask } from 'react-icons/tb/index';
|
||||
import { FaChurch, FaSubway, FaShoppingBasket } from 'react-icons/fa/index';
|
||||
import { TbScubaMask, TbBeerFilled } from 'react-icons/tb/index';
|
||||
import { GiVillage, GiCarousel } from 'react-icons/gi/index';
|
||||
import {
|
||||
MdTempleBuddhist,
|
||||
MdConstruction,
|
||||
MdLocationPin,
|
||||
} from 'react-icons/md/index';
|
||||
import { HiOfficeBuilding } from 'react-icons/hi/index';
|
||||
import { AiFillExperiment } from 'react-icons/ai/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
|
||||
@@ -49,6 +52,48 @@ export const places: Category = {
|
||||
label: 'Underwater',
|
||||
src: '/sounds/places/underwater.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbBeerFilled />,
|
||||
id: 'crowded-bar',
|
||||
label: 'Crowded Bar',
|
||||
src: '/sounds/places/crowded-bar.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiVillage />,
|
||||
id: 'night-village',
|
||||
label: 'Night Village',
|
||||
src: '/sounds/places/night-village.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaSubway />,
|
||||
id: 'subway-station',
|
||||
label: 'Subway Station',
|
||||
src: '/sounds/places/subway-station.mp3',
|
||||
},
|
||||
{
|
||||
icon: <HiOfficeBuilding />,
|
||||
id: 'office',
|
||||
label: 'Office',
|
||||
src: '/sounds/places/office.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaShoppingBasket />,
|
||||
id: 'supermarket',
|
||||
label: 'Supermarket',
|
||||
src: '/sounds/places/supermarket.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiCarousel />,
|
||||
id: 'carousel',
|
||||
label: 'Carousel',
|
||||
src: '/sounds/places/carousel.mp3',
|
||||
},
|
||||
{
|
||||
icon: <AiFillExperiment />,
|
||||
id: 'laboratory',
|
||||
label: 'Laboratory',
|
||||
src: '/sounds/places/laboratory.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Places',
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { GiWindchimes } from 'react-icons/gi/index';
|
||||
import { GiWindchimes, GiFilmProjector } from 'react-icons/gi/index';
|
||||
import { BsFillKeyboardFill } from 'react-icons/bs/index';
|
||||
import { FaKeyboard, FaClock, FaFan } from 'react-icons/fa/index';
|
||||
import { MdSmartToy } from 'react-icons/md/index';
|
||||
import { MdSmartToy, MdWaterDrop, MdRadio } from 'react-icons/md/index';
|
||||
import { TbBowlFilled } from 'react-icons/tb/index';
|
||||
import { RiFilePaper2Fill } from 'react-icons/ri/index';
|
||||
import { RiFilePaper2Fill, RiBubbleChartFill } from 'react-icons/ri/index';
|
||||
import { BiSolidDryer } from 'react-icons/bi/index';
|
||||
import { IoIosRadio } from 'react-icons/io/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
|
||||
@@ -53,6 +55,42 @@ export const things: Category = {
|
||||
label: 'Ceiling Fan',
|
||||
src: '/sounds/things/ceiling-fan.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidDryer />,
|
||||
id: 'dryer',
|
||||
label: 'Dryer',
|
||||
src: '/sounds/things/dryer.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiFilmProjector />,
|
||||
id: 'slide-projector',
|
||||
label: 'Slide Projector',
|
||||
src: '/sounds/things/slide-projector.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdWaterDrop />,
|
||||
id: 'boiling-water',
|
||||
label: 'Boiling Water',
|
||||
src: '/sounds/things/boiling-water.mp3',
|
||||
},
|
||||
{
|
||||
icon: <RiBubbleChartFill />,
|
||||
id: 'bubbles',
|
||||
label: 'Bubbles',
|
||||
src: '/sounds/things/bubbles.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdRadio />,
|
||||
id: 'tuning-radio',
|
||||
label: 'Tuning Radio',
|
||||
src: '/sounds/things/tuning-radio.mp3',
|
||||
},
|
||||
{
|
||||
icon: <IoIosRadio />,
|
||||
id: 'morse-code',
|
||||
label: 'Morse Code',
|
||||
src: '/sounds/things/morse-code.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Things',
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BiSolidTraffic } from 'react-icons/bi/index';
|
||||
import { FaCity, FaRoad } from 'react-icons/fa/index';
|
||||
import { PiRoadHorizonFill, PiSirenBold } from 'react-icons/pi/index';
|
||||
import { BsSoundwave, BsPeopleFill } from 'react-icons/bs/index';
|
||||
import { RiSparkling2Fill } from 'react-icons/ri/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
|
||||
@@ -45,6 +46,12 @@ export const urban: Category = {
|
||||
label: 'Traffic',
|
||||
src: '/sounds/urban/traffic.mp3',
|
||||
},
|
||||
{
|
||||
icon: <RiSparkling2Fill />,
|
||||
id: 'fireworks',
|
||||
label: 'Fireworks',
|
||||
src: '/sounds/urban/fireworks.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Urban',
|
||||
};
|
||||
|
||||
19
src/hooks/use-copy.ts
Normal file
19
src/hooks/use-copy.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useCopy(timeout = 1500) {
|
||||
const [copying, setCopying] = useState(false);
|
||||
|
||||
const copy = useCallback(
|
||||
(content: string) => {
|
||||
if (copying) return;
|
||||
|
||||
navigator.clipboard.writeText(content);
|
||||
setCopying(true);
|
||||
|
||||
setTimeout(() => setCopying(false), timeout);
|
||||
},
|
||||
[copying, timeout],
|
||||
);
|
||||
|
||||
return { copy, copying };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useEffect, useCallback, useState } from 'react';
|
||||
import { Howl } from 'howler';
|
||||
|
||||
import { useLoadingStore } from '@/store';
|
||||
import { useSSR } from './use-ssr';
|
||||
|
||||
export function useSound(
|
||||
@@ -8,7 +9,9 @@ export function useSound(
|
||||
options: { loop?: boolean; volume?: number } = {},
|
||||
) {
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isLoading = useLoadingStore(state => state.loaders[src]);
|
||||
const setIsLoading = useLoadingStore(state => state.set);
|
||||
|
||||
const { isBrowser } = useSSR();
|
||||
const sound = useMemo<Howl | null>(() => {
|
||||
let sound: Howl | null = null;
|
||||
@@ -16,7 +19,7 @@ export function useSound(
|
||||
if (isBrowser) {
|
||||
sound = new Howl({
|
||||
onload: () => {
|
||||
setIsLoading(false);
|
||||
setIsLoading(src, false);
|
||||
setHasLoaded(true);
|
||||
},
|
||||
preload: false,
|
||||
@@ -25,7 +28,7 @@ export function useSound(
|
||||
}
|
||||
|
||||
return sound;
|
||||
}, [src, isBrowser]);
|
||||
}, [src, isBrowser, setIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sound) {
|
||||
@@ -41,7 +44,7 @@ export function useSound(
|
||||
const play = useCallback(() => {
|
||||
if (sound) {
|
||||
if (!hasLoaded && !isLoading) {
|
||||
setIsLoading(true);
|
||||
setIsLoading(src, true);
|
||||
sound.load();
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ export function useSound(
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
}, [sound, hasLoaded, isLoading]);
|
||||
}, [src, setIsLoading, sound, hasLoaded, isLoading]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
if (sound) sound.stop();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import AboutSection from '@/components/sections/about.astro';
|
||||
@@ -10,6 +11,7 @@ import { App } from '@/components/app';
|
||||
---
|
||||
|
||||
<Layout title="Moodist: Ambient Sounds for Focus and Calm">
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load />
|
||||
<AboutSection />
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { useSoundStore } from './sound';
|
||||
export { useLoadingStore } from './loading';
|
||||
|
||||
13
src/store/loading/index.ts
Normal file
13
src/store/loading/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface LoadingStore {
|
||||
loaders: Record<string, boolean>;
|
||||
set: (id: string, value: boolean) => void;
|
||||
}
|
||||
|
||||
export const useLoadingStore = create<LoadingStore>()((set, get) => ({
|
||||
loaders: {},
|
||||
set(id: string, value: boolean) {
|
||||
set({ loaders: { ...get().loaders, [id]: value } });
|
||||
},
|
||||
}));
|
||||
@@ -5,6 +5,7 @@ import type { SoundState } from './sound.state';
|
||||
import { pickMany, random } from '@/helpers/random';
|
||||
|
||||
export interface SoundActions {
|
||||
override: (sounds: Record<string, number>) => void;
|
||||
pause: () => void;
|
||||
play: () => void;
|
||||
restoreHistory: () => void;
|
||||
@@ -24,6 +25,19 @@ export const createActions: StateCreator<
|
||||
SoundActions
|
||||
> = (set, get) => {
|
||||
return {
|
||||
override(newSounds) {
|
||||
get().unselectAll();
|
||||
|
||||
const sounds = get().sounds;
|
||||
|
||||
Object.keys(newSounds).forEach(sound => {
|
||||
sounds[sound].isSelected = true;
|
||||
sounds[sound].volume = newSounds[sound];
|
||||
});
|
||||
|
||||
set({ sounds: { ...sounds } });
|
||||
},
|
||||
|
||||
pause() {
|
||||
set({ isPlaying: false });
|
||||
},
|
||||
|
||||
@@ -33,3 +33,12 @@
|
||||
src: url('/fonts/inter-tight-v7-latin-600.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* inter-tight-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('/fonts/inter-tight-v7-latin-700.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user