From fcbe50c78c30e4422aea2ed698fff777fcaea1c4 Mon Sep 17 00:00:00 2001 From: MAZE Date: Sat, 12 Jul 2025 12:32:49 +0330 Subject: [PATCH] feat: add lofi music play --- package-lock.json | 56 ++++++++++++++ package.json | 1 + src/components/modals/lofi/index.ts | 1 + src/components/modals/lofi/lofi.module.css | 86 ++++++++++++++++++++++ src/components/modals/lofi/lofi.tsx | 85 +++++++++++++++++++++ src/components/toolbar/menu/items/index.ts | 1 + src/components/toolbar/menu/items/lofi.tsx | 11 +++ src/components/toolbar/menu/menu.tsx | 5 ++ 8 files changed, 246 insertions(+) create mode 100644 src/components/modals/lofi/index.ts create mode 100644 src/components/modals/lofi/lofi.module.css create mode 100644 src/components/modals/lofi/lofi.tsx create mode 100644 src/components/toolbar/menu/items/lofi.tsx diff --git a/package-lock.json b/package-lock.json index 30ad9d0..ee63aa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react-hotkeys-hook": "3.2.1", "react-icons": "4.11.0", "react-wrap-balancer": "1.1.0", + "react-youtube": "10.1.0", "uuid": "10.0.0", "zustand": "4.4.3" }, @@ -19285,6 +19286,12 @@ "node": ">=4" } }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", + "license": "MIT" + }, "node_modules/load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -22343,6 +22350,23 @@ "react": ">=16.8.0 || ^17.0.0 || ^18" } }, + "node_modules/react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + }, + "engines": { + "node": ">= 14.x" + }, + "peerDependencies": { + "react": ">=0.14.1" + } + }, "node_modules/read-pkg": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", @@ -23844,6 +23868,12 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==", + "license": "BSD-3-Clause" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -27593,6 +27623,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + } + }, + "node_modules/youtube-player/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/youtube-player/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index a06e5b1..da0abd4 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-hotkeys-hook": "3.2.1", "react-icons": "4.11.0", "react-wrap-balancer": "1.1.0", + "react-youtube": "10.1.0", "uuid": "10.0.0", "zustand": "4.4.3" }, diff --git a/src/components/modals/lofi/index.ts b/src/components/modals/lofi/index.ts new file mode 100644 index 0000000..5cf9a89 --- /dev/null +++ b/src/components/modals/lofi/index.ts @@ -0,0 +1 @@ +export { LofiModal } from './lofi'; diff --git a/src/components/modals/lofi/lofi.module.css b/src/components/modals/lofi/lofi.module.css new file mode 100644 index 0000000..807782b --- /dev/null +++ b/src/components/modals/lofi/lofi.module.css @@ -0,0 +1,86 @@ +.title { + margin-bottom: 12px; + font-family: var(--font-heading); + font-weight: 600; +} + +.notice { + & p { + line-height: 1.4; + color: var(--color-foreground-subtle); + } + + & .buttons { + display: flex; + column-gap: 8px; + align-items: center; + justify-content: flex-end; + margin-top: 12px; + + & button { + display: flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 16px; + font-size: var(--font-sm); + font-weight: 500; + color: var(--color-foreground); + cursor: pointer; + background: var(--color-neutral-200); + border: none; + border-radius: 8px; + + &.primary { + color: var(--color-neutral-50); + background: var(--color-neutral-950); + } + } + } +} + +.videos { + margin-top: 20px; + + & .video { + &:not(:last-of-type) { + margin-bottom: 24px; + } + + & h2 { + margin-bottom: 8px; + font-size: var(--font-sm); + color: var(--color-foreground-subtle); + + & span { + display: inline-block; + color: var(--color-foreground-subtler); + + &.index { + margin-right: 4px; + } + } + + & strong { + font-weight: 500; + color: var(--color-foreground); + } + } + + & .container { + padding: 8px; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-300); + border-radius: 12px; + + & .iframe { + width: 100%; + max-width: 100%; + height: auto; + aspect-ratio: 560 / 315; + border: 1px solid var(--color-neutral-200); + border-radius: 8px; + } + } + } +} diff --git a/src/components/modals/lofi/lofi.tsx b/src/components/modals/lofi/lofi.tsx new file mode 100644 index 0000000..836b3ae --- /dev/null +++ b/src/components/modals/lofi/lofi.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react'; +import YouTube from 'react-youtube'; + +import { Modal } from '@/components/modal/modal'; + +import styles from './lofi.module.css'; +import { padNumber } from '@/helpers/number'; + +interface LofiProps { + onClose: () => void; + show: boolean; +} + +const videos = [ + { + channel: 'Lofi Girl', + id: 'jfKfPfyJRdk', + title: 'lofi hip hop radio', + }, + { + channel: 'Lofi Girl', + id: '4xDzrJKXOOY', + title: 'synthwave radio', + }, + { + channel: 'Lofi Girl', + id: 'P6Segk8cr-c', + title: 'sad lofi radio', + }, + { + channel: 'Lofi Girl', + id: 'S_MOd40zlYU', + title: 'dark ambient radio', + }, + { + channel: 'Lofi Girl', + id: 'TtkFsfOP9QI', + title: 'peaceful piano radio', + }, +]; + +export function LofiModal({ onClose, show }: LofiProps) { + const [isAccepted, setIsAccepted] = useState(false); + + return ( + +

Lofi Music Player

+ + {!isAccepted ? ( +
+

+ This feature plays music using embedded YouTube videos. By + continuing, you agree to connect to YouTube, which may collect data + in accordance with their privacy policy. We do not control or track + this data. +

+ +
+ + +
+
+ ) : ( +
+ {videos.map((video, index) => ( +
+

+ {padNumber(index + 1, 2)}{' '} + {video.channel} / {video.title} +

+
+ +
+
+ ))} +
+ )} +
+ ); +} diff --git a/src/components/toolbar/menu/items/index.ts b/src/components/toolbar/menu/items/index.ts index 158ff5f..c83f5ee 100644 --- a/src/components/toolbar/menu/items/index.ts +++ b/src/components/toolbar/menu/items/index.ts @@ -12,3 +12,4 @@ export { Todo as TodoItem } from './todo'; export { Countdown as CountdownItem } from './countdown'; export { Binaural as BinauralItem } from './binaural'; export { Isochronic as IsochronicItem } from './isochronic'; +export { Lofi as LofiItem } from './lofi'; diff --git a/src/components/toolbar/menu/items/lofi.tsx b/src/components/toolbar/menu/items/lofi.tsx new file mode 100644 index 0000000..53bd4e8 --- /dev/null +++ b/src/components/toolbar/menu/items/lofi.tsx @@ -0,0 +1,11 @@ +import { FaHeadphonesAlt } from 'react-icons/fa/index'; + +import { Item } from '../item'; + +interface LofiProps { + open: () => void; +} + +export function Lofi({ open }: LofiProps) { + return } label="Lofi Music" onClick={open} />; +} diff --git a/src/components/toolbar/menu/menu.tsx b/src/components/toolbar/menu/menu.tsx index f7948f4..a04b596 100644 --- a/src/components/toolbar/menu/menu.tsx +++ b/src/components/toolbar/menu/menu.tsx @@ -19,6 +19,7 @@ import { CountdownItem, BinauralItem, IsochronicItem, + LofiItem, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; @@ -28,6 +29,7 @@ import { SleepTimerModal } from '@/components/modals/sleep-timer'; import { BreathingExerciseModal } from '@/components/modals/breathing'; import { BinauralModal } from '@/components/modals/binaural'; import { IsochronicModal } from '@/components/modals/isochronic'; +import { LofiModal } from '@/components/modals/lofi'; import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox'; import { Slider } from '@/components/slider'; @@ -51,6 +53,7 @@ export function Menu() { breathing: false, countdown: false, isochronic: false, + lofi: false, notepad: false, pomodoro: false, presets: false, @@ -137,6 +140,7 @@ export function Menu() { open('binaural')} /> open('isochronic')} /> + open('lofi')} /> open('shortcuts')} /> @@ -193,6 +197,7 @@ export function Menu() { show={modals.isochronic} onClose={() => close('isochronic')} /> + close('lofi')} /> ); }