loukai-app 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/README.md +558 -0
  2. package/bin/loukai.js +32 -0
  3. package/package.json +243 -0
  4. package/src/main/appState.js +250 -0
  5. package/src/main/audioEngine.js +478 -0
  6. package/src/main/creator/conversionService.js +503 -0
  7. package/src/main/creator/downloadManager.js +1128 -0
  8. package/src/main/creator/ffmpegService.js +487 -0
  9. package/src/main/creator/installLogger.js +51 -0
  10. package/src/main/creator/keyDetection.js +212 -0
  11. package/src/main/creator/llmService.js +370 -0
  12. package/src/main/creator/lrclibService.js +340 -0
  13. package/src/main/creator/python/crepe_runner.py +189 -0
  14. package/src/main/creator/python/demucs_runner.py +158 -0
  15. package/src/main/creator/python/whisper_runner.py +172 -0
  16. package/src/main/creator/pythonRunner.js +268 -0
  17. package/src/main/creator/stemBuilder.js +491 -0
  18. package/src/main/creator/systemChecker.js +474 -0
  19. package/src/main/handlers/appHandlers.js +45 -0
  20. package/src/main/handlers/audioHandlers.js +33 -0
  21. package/src/main/handlers/autotuneHandlers.js +28 -0
  22. package/src/main/handlers/canvasHandlers.js +84 -0
  23. package/src/main/handlers/creatorHandlers.js +159 -0
  24. package/src/main/handlers/editorHandlers.js +98 -0
  25. package/src/main/handlers/effectsHandlers.js +100 -0
  26. package/src/main/handlers/fileHandlers.js +45 -0
  27. package/src/main/handlers/index.js +78 -0
  28. package/src/main/handlers/libraryHandlers.js +96 -0
  29. package/src/main/handlers/mixerHandlers.js +64 -0
  30. package/src/main/handlers/playerHandlers.js +39 -0
  31. package/src/main/handlers/preferencesHandlers.js +46 -0
  32. package/src/main/handlers/queueHandlers.js +81 -0
  33. package/src/main/handlers/rendererHandlers.js +63 -0
  34. package/src/main/handlers/settingsHandlers.js +42 -0
  35. package/src/main/handlers/webServerHandlers.js +105 -0
  36. package/src/main/main.js +2351 -0
  37. package/src/main/preload.js +252 -0
  38. package/src/main/settingsManager.js +139 -0
  39. package/src/main/statePersistence.js +193 -0
  40. package/src/main/utils/pathValidator.js +112 -0
  41. package/src/main/webServer.js +2535 -0
  42. package/src/native/autotune.js +417 -0
  43. package/src/renderer/adapters/ElectronBridge.js +677 -0
  44. package/src/renderer/canvas.html +80 -0
  45. package/src/renderer/components/App.jsx +303 -0
  46. package/src/renderer/components/AppRoot.jsx +37 -0
  47. package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
  48. package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
  49. package/src/renderer/components/MixerTab.jsx +233 -0
  50. package/src/renderer/components/MixerTabWrapper.jsx +31 -0
  51. package/src/renderer/components/PortalSelect.jsx +239 -0
  52. package/src/renderer/components/QueueTab.jsx +116 -0
  53. package/src/renderer/components/RequestsListWrapper.jsx +78 -0
  54. package/src/renderer/components/ServerTab.jsx +472 -0
  55. package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
  56. package/src/renderer/components/StatusBar.jsx +92 -0
  57. package/src/renderer/components/TabNavigation.jsx +77 -0
  58. package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
  59. package/src/renderer/components/creator/CreateTab.jsx +1236 -0
  60. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
  61. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
  62. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
  63. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
  64. package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
  65. package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
  66. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
  67. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
  68. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
  69. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
  70. package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
  71. package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
  72. package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
  73. package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
  74. package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
  75. package/src/renderer/dist/renderer.css +1 -0
  76. package/src/renderer/dist/renderer.js +62 -0
  77. package/src/renderer/dist/renderer.js.map +1 -0
  78. package/src/renderer/dist/renderer.woff2 +0 -0
  79. package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
  80. package/src/renderer/index.html +24 -0
  81. package/src/renderer/index.html.backup +372 -0
  82. package/src/renderer/js/PlayerInterface.js +267 -0
  83. package/src/renderer/js/autoTuneWorklet.js +224 -0
  84. package/src/renderer/js/butterchurnVerify.js +46 -0
  85. package/src/renderer/js/canvas-app.js +114 -0
  86. package/src/renderer/js/cdgPlayer.js +685 -0
  87. package/src/renderer/js/kaiPlayer.js +1200 -0
  88. package/src/renderer/js/karaokeRenderer.js +3392 -0
  89. package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
  90. package/src/renderer/js/microphoneEngine.js +656 -0
  91. package/src/renderer/js/musicAnalysisWorklet.js +216 -0
  92. package/src/renderer/js/phaseVocoderWorklet.js +341 -0
  93. package/src/renderer/js/player.js +232 -0
  94. package/src/renderer/js/referencePitchTracker.js +130 -0
  95. package/src/renderer/js/songLoaders.js +334 -0
  96. package/src/renderer/js/soundtouch-worklet.js +1395 -0
  97. package/src/renderer/js/webrtcManager.js +511 -0
  98. package/src/renderer/lib/butterchurn.min.js +6739 -0
  99. package/src/renderer/lib/butterchurnPresets.min.js +1 -0
  100. package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
  101. package/src/renderer/lib/cdgraphics.js +299 -0
  102. package/src/renderer/public/js/autoTuneWorklet.js +224 -0
  103. package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
  104. package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
  105. package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
  106. package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
  107. package/src/renderer/react-entry.jsx +44 -0
  108. package/src/renderer/styles/tailwind.css +106 -0
  109. package/src/renderer/utils/qrCodeGenerator.js +98 -0
  110. package/src/renderer/vite.config.js +31 -0
  111. package/src/shared/adapters/BridgeInterface.js +195 -0
  112. package/src/shared/components/EffectsPanel.jsx +177 -0
  113. package/src/shared/components/LibraryPanel.jsx +701 -0
  114. package/src/shared/components/LineDetailCanvas.jsx +167 -0
  115. package/src/shared/components/LyricLine.jsx +505 -0
  116. package/src/shared/components/LyricRejection.jsx +84 -0
  117. package/src/shared/components/LyricSuggestion.jsx +80 -0
  118. package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
  119. package/src/shared/components/MixerPanel.jsx +94 -0
  120. package/src/shared/components/PlayerControls.jsx +206 -0
  121. package/src/shared/components/PortalSelect.jsx +239 -0
  122. package/src/shared/components/QueueList.jsx +365 -0
  123. package/src/shared/components/QuickSearch.jsx +126 -0
  124. package/src/shared/components/RequestsList.jsx +121 -0
  125. package/src/shared/components/SongEditor.jsx +1362 -0
  126. package/src/shared/components/SongInfoBar.jsx +81 -0
  127. package/src/shared/components/ThemeToggle.jsx +106 -0
  128. package/src/shared/components/Toast.jsx +30 -0
  129. package/src/shared/components/VisualizationSettings.jsx +243 -0
  130. package/src/shared/constants.js +95 -0
  131. package/src/shared/context/BridgeContext.jsx +32 -0
  132. package/src/shared/contexts/AudioContext.jsx +37 -0
  133. package/src/shared/contexts/PlayerContext.jsx +66 -0
  134. package/src/shared/contexts/SettingsContext.jsx +50 -0
  135. package/src/shared/defaults.js +158 -0
  136. package/src/shared/formatUtils.js +59 -0
  137. package/src/shared/formatUtils.test.js +207 -0
  138. package/src/shared/hooks/useAppState.js +97 -0
  139. package/src/shared/hooks/useAudioEngine.js +264 -0
  140. package/src/shared/hooks/usePlayer.js +89 -0
  141. package/src/shared/hooks/useSettingsPersistence.js +74 -0
  142. package/src/shared/hooks/useWebRTC.js +118 -0
  143. package/src/shared/ipcContracts.js +299 -0
  144. package/src/shared/package.json +3 -0
  145. package/src/shared/services/creatorService.js +373 -0
  146. package/src/shared/services/creatorService.test.js +413 -0
  147. package/src/shared/services/editorService.js +213 -0
  148. package/src/shared/services/editorService.test.js +219 -0
  149. package/src/shared/services/effectsService.js +271 -0
  150. package/src/shared/services/effectsService.test.js +418 -0
  151. package/src/shared/services/libraryService.js +438 -0
  152. package/src/shared/services/libraryService.test.js +474 -0
  153. package/src/shared/services/mixerService.js +172 -0
  154. package/src/shared/services/mixerService.test.js +399 -0
  155. package/src/shared/services/playerService.js +221 -0
  156. package/src/shared/services/playerService.test.js +357 -0
  157. package/src/shared/services/preferencesService.js +219 -0
  158. package/src/shared/services/queueService.js +226 -0
  159. package/src/shared/services/queueService.test.js +430 -0
  160. package/src/shared/services/requestsService.js +155 -0
  161. package/src/shared/services/requestsService.test.js +362 -0
  162. package/src/shared/services/serverSettingsService.js +151 -0
  163. package/src/shared/services/settingsService.js +257 -0
  164. package/src/shared/services/settingsService.test.js +295 -0
  165. package/src/shared/state/StateManager.js +263 -0
  166. package/src/shared/utils/audio.js +42 -0
  167. package/src/shared/utils/format.js +32 -0
  168. package/src/shared/utils/lyricsUtils.js +162 -0
  169. package/src/test/setup.js +40 -0
  170. package/src/utils/cdgLoader.js +180 -0
  171. package/src/utils/m4aLoader.js +333 -0
  172. package/src/web/App.jsx +578 -0
  173. package/src/web/adapters/WebBridge.js +428 -0
  174. package/src/web/components/PlayerSettingsPanel.jsx +231 -0
  175. package/src/web/components/SongSearch.jsx +180 -0
  176. package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
  177. package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
  178. package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
  179. package/src/web/dist/index.html +15 -0
  180. package/src/web/index.html +14 -0
  181. package/src/web/main.jsx +10 -0
  182. package/src/web/package-lock.json +1765 -0
  183. package/src/web/pages/SongRequestPage.jsx +619 -0
  184. package/src/web/styles/tailwind.css +68 -0
  185. package/src/web/vite.config.js +27 -0
  186. package/static/fonts/material-icons.woff2 +0 -0
  187. package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
  188. package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
  189. package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
  190. package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
  191. package/static/images/butterchurn-screenshots/An AdamFX n Martin Infusion 2 flexi - Why The Sky Looks Diffrent Today - AdamFx n Martin Infusion - Tack Tile Disfunction B.png +0 -0
  192. package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
  193. proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
  194. package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
  195. package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
  196. package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
  197. package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
  198. package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
  199. package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
  200. package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
  201. package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
  202. package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
  203. package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
  204. package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
  205. package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
  206. package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
  207. package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
  208. package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
  209. package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
  210. package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
  211. package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
  212. package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
  213. package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
  214. package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
  215. package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
  216. package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
  217. package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
  218. package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
  219. package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
  220. package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
  221. package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
  222. package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
  223. package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
  224. package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
  225. package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
  226. package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
  227. package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
  228. package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
  229. package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
  230. package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
  231. package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
  232. package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
  233. package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
  234. package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
  235. package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
  236. package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
  237. package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
  238. package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
  239. package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
  240. package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
  241. package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
  242. package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
  243. package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
  244. package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
  245. package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
  246. package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
  247. package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
  248. package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
  249. package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
  250. package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
  251. package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
  252. package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
  253. package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
  254. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
  255. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
  256. package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
  257. package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
  258. package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
  259. package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
  260. package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
  261. package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
  262. package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
  263. package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
  264. package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
  265. package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
  266. package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
  267. package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
  268. package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
  269. package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
  270. package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
  271. package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
  272. package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
  273. package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
  274. package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
  275. package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
  276. package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
  277. package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
  278. package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
  279. package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
  280. package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
  281. package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
  282. package/static/images/butterchurn-screenshots/suksma - Rovastar - Sunflower Passion _Enlightment Mix__Phat_edit _ flexi und martin shaders - circumflex in character classes in regular expression.png +0 -0
  283. package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
  284. package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
  285. package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
  286. package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
  287. package/static/images/logo-512.png +0 -0
  288. package/static/images/logo.png +0 -0
  289. package/static/loukai-logo.png +0 -0
  290. package/static/screenshot-generator.html +610 -0
@@ -0,0 +1,619 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { io } from 'socket.io-client';
3
+ import { getFormatIcon, formatDuration } from '../../shared/formatUtils.js';
4
+ import { Toast } from '../../shared/components/Toast.jsx';
5
+ import { ThemeToggle } from '../../shared/components/ThemeToggle.jsx';
6
+
7
+ export function SongRequestPage() {
8
+ const [userName, setUserName] = useState(null);
9
+ const [nameInput, setNameInput] = useState('');
10
+ const [serverName, setServerName] = useState('Loukai Karaoke');
11
+ const [allowRequests, setAllowRequests] = useState(true);
12
+ const [songs, setSongs] = useState([]);
13
+ const [availableLetters, setAvailableLetters] = useState([]);
14
+ const [currentLetter, setCurrentLetter] = useState('A');
15
+ const [currentPage, setCurrentPage] = useState(1);
16
+ const [totalPages, setTotalPages] = useState(1);
17
+ const [queue, setQueue] = useState([]);
18
+ const [quickSearchTerm, setQuickSearchTerm] = useState('');
19
+ const [quickSearchResults, setQuickSearchResults] = useState([]);
20
+ const [showQuickSearch, setShowQuickSearch] = useState(false);
21
+ const [selectedSong, setSelectedSong] = useState(null);
22
+ const [showRequestModal, setShowRequestModal] = useState(false);
23
+ const [requestMessage, setRequestMessage] = useState('');
24
+ const [toast, setToast] = useState(null);
25
+
26
+ const socketRef = useRef(null);
27
+ const quickSearchRef = useRef(null);
28
+
29
+ const showToast = (message, type = 'info') => {
30
+ setToast({ message, type });
31
+ };
32
+
33
+ // Load user name from localStorage on mount
34
+ useEffect(() => {
35
+ const storedName = localStorage.getItem('karaoke-user-name');
36
+ if (storedName && storedName.trim()) {
37
+ setUserName(storedName.trim());
38
+ }
39
+ }, []);
40
+
41
+ // Initialize socket connection
42
+ useEffect(() => {
43
+ socketRef.current = io();
44
+
45
+ socketRef.current.on('queue-update', (data) => {
46
+ setQueue(data.queue || []);
47
+ });
48
+
49
+ return () => {
50
+ socketRef.current?.disconnect();
51
+ };
52
+ }, []);
53
+
54
+ // Load server info when user is set
55
+ useEffect(() => {
56
+ if (!userName) return;
57
+
58
+ fetch('/api/info')
59
+ .then((res) => res.json())
60
+ .then((info) => {
61
+ setServerName(info.serverName || 'Loukai Karaoke');
62
+ setAllowRequests(info.allowRequests !== false);
63
+ document.title = `${info.serverName || 'Karaoke'} - Song Requests`;
64
+ })
65
+ .catch((err) => console.error('Failed to load server info:', err));
66
+ }, [userName]);
67
+
68
+ // Load available letters when user is set
69
+ useEffect(() => {
70
+ if (!userName) return;
71
+
72
+ fetch('/api/letters')
73
+ .then((res) => res.json())
74
+ .then((data) => {
75
+ const letters = data.letters || [];
76
+ setAvailableLetters(letters);
77
+ const firstLetter = letters.includes('A') ? 'A' : letters[0];
78
+ if (firstLetter) {
79
+ loadLetterPage(firstLetter, 1);
80
+ }
81
+ })
82
+ .catch((err) => console.error('Failed to load letters:', err));
83
+ }, [userName]);
84
+
85
+ // Load queue periodically
86
+ useEffect(() => {
87
+ if (!userName) return;
88
+
89
+ const loadQueue = () => {
90
+ fetch('/api/queue')
91
+ .then((res) => res.json())
92
+ .then((data) => setQueue(data.queue || []))
93
+ .catch((err) => console.error('Failed to load queue:', err));
94
+ };
95
+
96
+ loadQueue();
97
+ const interval = setInterval(loadQueue, 10000);
98
+ return () => clearInterval(interval);
99
+ }, [userName]);
100
+
101
+ // Quick search handler
102
+ useEffect(() => {
103
+ if (!quickSearchTerm.trim()) {
104
+ setQuickSearchResults([]);
105
+ setShowQuickSearch(false);
106
+ return;
107
+ }
108
+
109
+ const search = async () => {
110
+ try {
111
+ const res = await fetch(`/api/search?q=${encodeURIComponent(quickSearchTerm)}`);
112
+ const data = await res.json();
113
+ setQuickSearchResults(data.results || []);
114
+ setShowQuickSearch(true);
115
+ } catch (err) {
116
+ console.error('Search failed:', err);
117
+ setQuickSearchResults([]);
118
+ }
119
+ };
120
+
121
+ const debounce = setTimeout(search, 300);
122
+ return () => clearTimeout(debounce);
123
+ }, [quickSearchTerm]);
124
+
125
+ // Click outside to close quick search
126
+ useEffect(() => {
127
+ const handleClickOutside = (e) => {
128
+ if (quickSearchRef.current && !quickSearchRef.current.contains(e.target)) {
129
+ setShowQuickSearch(false);
130
+ }
131
+ };
132
+
133
+ document.addEventListener('mousedown', handleClickOutside);
134
+ return () => document.removeEventListener('mousedown', handleClickOutside);
135
+ }, []);
136
+
137
+ const loadLetterPage = async (letter, page) => {
138
+ try {
139
+ const res = await fetch(
140
+ `/api/songs/letter/${encodeURIComponent(letter)}?page=${page}&limit=50`
141
+ );
142
+ const data = await res.json();
143
+
144
+ setSongs(data.songs || []);
145
+ setCurrentLetter(letter);
146
+ setCurrentPage(page);
147
+ setTotalPages(data.pagination?.totalPages || 1);
148
+ } catch (err) {
149
+ console.error('Failed to load songs:', err);
150
+ setSongs([]);
151
+ }
152
+ };
153
+
154
+ const handleNameSubmit = () => {
155
+ const name = nameInput.trim();
156
+ if (name) {
157
+ setUserName(name);
158
+ localStorage.setItem('karaoke-user-name', name);
159
+ }
160
+ };
161
+
162
+ const handleRequestSong = (song) => {
163
+ if (!allowRequests) return;
164
+ setSelectedSong(song);
165
+ setRequestMessage('');
166
+ setShowRequestModal(true);
167
+ };
168
+
169
+ const submitRequest = async () => {
170
+ if (!selectedSong) return;
171
+
172
+ try {
173
+ const res = await fetch('/api/request', {
174
+ method: 'POST',
175
+ headers: { 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({
177
+ songId: selectedSong.path,
178
+ requesterName: userName,
179
+ message: requestMessage,
180
+ }),
181
+ });
182
+
183
+ if (res.ok) {
184
+ const data = await res.json();
185
+ setShowRequestModal(false);
186
+ setSelectedSong(null);
187
+ setRequestMessage('');
188
+ showToast(data.message || 'Request submitted!', 'success');
189
+ } else {
190
+ const error = await res.json();
191
+ showToast(error.error || 'Request failed', 'error');
192
+ }
193
+ } catch (err) {
194
+ console.error('Request failed:', err);
195
+ showToast('Failed to submit request', 'error');
196
+ }
197
+ };
198
+
199
+ // Smart pagination - show limited page numbers around current page
200
+ const getPageNumbers = () => {
201
+ const maxButtons = 7; // Show max 7 page buttons
202
+ if (totalPages <= maxButtons) {
203
+ // Show all pages if total is small
204
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
205
+ }
206
+
207
+ const pages = [];
208
+ const halfRange = Math.floor((maxButtons - 3) / 2); // Reserve 3 for first, last, and ellipsis
209
+
210
+ // Always show first page
211
+ pages.push(1);
212
+
213
+ let startPage = Math.max(2, currentPage - halfRange);
214
+ let endPage = Math.min(totalPages - 1, currentPage + halfRange);
215
+
216
+ // Adjust if we're near the beginning
217
+ if (currentPage <= halfRange + 2) {
218
+ endPage = Math.min(maxButtons - 1, totalPages - 1);
219
+ }
220
+
221
+ // Adjust if we're near the end
222
+ if (currentPage >= totalPages - halfRange - 1) {
223
+ startPage = Math.max(2, totalPages - maxButtons + 2);
224
+ }
225
+
226
+ // Add ellipsis if needed before
227
+ if (startPage > 2) {
228
+ pages.push('...');
229
+ }
230
+
231
+ // Add middle pages
232
+ for (let i = startPage; i <= endPage; i++) {
233
+ pages.push(i);
234
+ }
235
+
236
+ // Add ellipsis if needed after
237
+ if (endPage < totalPages - 1) {
238
+ pages.push('...');
239
+ }
240
+
241
+ // Always show last page
242
+ if (totalPages > 1) {
243
+ pages.push(totalPages);
244
+ }
245
+
246
+ return pages;
247
+ };
248
+
249
+ const allLetters = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), '#'];
250
+
251
+ // Name prompt modal
252
+ if (!userName) {
253
+ return (
254
+ <div className="fixed top-0 left-0 w-full h-full bg-black/90 flex items-center justify-center z-[1000]">
255
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-8 max-w-md w-[90%] text-center border border-gray-300 dark:border-gray-600">
256
+ <div className="text-3xl font-bold mb-6 text-gray-900 dark:text-white">
257
+ Loukai Karaoke
258
+ </div>
259
+ <div className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
260
+ Please enter your name to request songs
261
+ </div>
262
+ <input
263
+ type="text"
264
+ className="w-full px-3 py-3 border-2 border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-white text-base mb-6 focus:outline-none focus:border-blue-600 dark:focus:border-blue-500"
265
+ placeholder="Your name..."
266
+ maxLength={50}
267
+ value={nameInput}
268
+ onChange={(e) => setNameInput(e.target.value)}
269
+ onKeyPress={(e) => e.key === 'Enter' && nameInput.trim() && handleNameSubmit()}
270
+ autoFocus
271
+ />
272
+ <button
273
+ className="bg-blue-600 text-white border-none px-6 py-3 rounded-lg text-base cursor-pointer w-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
274
+ disabled={!nameInput.trim()}
275
+ onClick={handleNameSubmit}
276
+ >
277
+ Continue
278
+ </button>
279
+ </div>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ // Main content
285
+ return (
286
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white">
287
+ <div className="bg-white dark:bg-gray-800 py-8 px-8 text-center border-b-2 border-blue-600 dark:border-blue-500 relative">
288
+ <div className="absolute top-4 right-4">
289
+ <ThemeToggle />
290
+ </div>
291
+ <h1 className="m-0 mb-2 text-4xl text-gray-900 dark:text-white">{serverName}</h1>
292
+ <div className="text-gray-600 dark:text-gray-400 text-lg">Request your favorite songs!</div>
293
+ </div>
294
+
295
+ <div className="max-w-6xl mx-auto p-5">
296
+ {/* Quick Search */}
297
+ <div
298
+ className="bg-white dark:bg-gray-800 rounded-lg p-4 mb-5 border border-gray-200 dark:border-gray-700 relative"
299
+ ref={quickSearchRef}
300
+ >
301
+ <div className="text-base mb-2 flex items-center gap-2 text-gray-900 dark:text-white">
302
+ <span className="material-icons">search</span>
303
+ <span>Quick Song Search</span>
304
+ </div>
305
+ <input
306
+ type="text"
307
+ className="w-full px-3 py-3 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-md text-gray-900 dark:text-white text-base focus:outline-none focus:border-blue-600 dark:focus:border-blue-500 focus:bg-white dark:focus:bg-gray-600"
308
+ placeholder="Search songs to request..."
309
+ value={quickSearchTerm}
310
+ onChange={(e) => setQuickSearchTerm(e.target.value)}
311
+ onFocus={() => quickSearchTerm && setShowQuickSearch(true)}
312
+ />
313
+ {showQuickSearch && (
314
+ <div className="absolute top-full left-4 right-4 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 border-t-0 rounded-b-md max-h-[300px] overflow-y-auto z-[1000] -mt-px">
315
+ {quickSearchResults.length === 0 ? (
316
+ <div className="p-5 text-center text-gray-500 dark:text-gray-400">
317
+ No songs found
318
+ </div>
319
+ ) : (
320
+ quickSearchResults.slice(0, 8).map((song) => (
321
+ <div
322
+ key={song.path}
323
+ className="p-3.5 cursor-pointer border-b border-gray-200 dark:border-gray-600 flex justify-between items-start gap-3 transition-colors hover:bg-gray-200 dark:hover:bg-gray-600 last:border-b-0"
324
+ onClick={() => {
325
+ handleRequestSong(song);
326
+ setShowQuickSearch(false);
327
+ setQuickSearchTerm('');
328
+ }}
329
+ >
330
+ <div className="flex-1 flex flex-col items-start">
331
+ <div className="flex items-baseline gap-3 flex-wrap">
332
+ <div className="font-semibold text-[0.95rem] inline-flex items-center gap-2 text-gray-900 dark:text-white">
333
+ <span className="text-xs">{getFormatIcon(song.format)}</span>
334
+ {song.title}
335
+ </div>
336
+ <div className="text-[0.9rem] text-gray-600 dark:text-gray-400">
337
+ {song.artist}
338
+ </div>
339
+ </div>
340
+ {(song.album || song.year || song.genre) && (
341
+ <div className="text-[0.82rem] text-gray-500 dark:text-gray-500 mt-1">
342
+ {song.album && <span>{song.album}</span>}
343
+ {song.year && (
344
+ <span className="before:content-['_•_'] before:text-gray-400 dark:before:text-gray-600">
345
+ {song.year}
346
+ </span>
347
+ )}
348
+ {song.genre && (
349
+ <span className="before:content-['_•_'] before:text-gray-400 dark:before:text-gray-600">
350
+ {song.genre}
351
+ </span>
352
+ )}
353
+ </div>
354
+ )}
355
+ </div>
356
+ <span className="text-gray-500 dark:text-gray-400 text-[0.9rem] font-medium">
357
+ {formatDuration(song.duration)}
358
+ </span>
359
+ </div>
360
+ ))
361
+ )}
362
+ </div>
363
+ )}
364
+ </div>
365
+
366
+ {/* Songs Section */}
367
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-5 mb-5 border border-gray-200 dark:border-gray-700">
368
+ <div className="flex justify-between items-center mb-4">
369
+ <div className="text-xl font-semibold text-gray-900 dark:text-white">
370
+ Available Songs
371
+ </div>
372
+ <div className="text-gray-500 dark:text-gray-400 text-sm">{songs.length} songs</div>
373
+ </div>
374
+
375
+ {/* Alphabet Navigation */}
376
+ <div className="mb-4">
377
+ <div className="text-sm text-gray-600 dark:text-gray-300 mb-2">Browse by Artist:</div>
378
+ <div className="flex flex-wrap gap-1.5">
379
+ {allLetters.map((letter) => {
380
+ const hasContent = availableLetters.includes(letter);
381
+ return (
382
+ <button
383
+ key={letter}
384
+ className={`px-3 py-2 rounded border transition-all text-sm min-w-[40px] ${
385
+ currentLetter === letter
386
+ ? 'bg-blue-600 dark:bg-blue-500 border-blue-600 dark:border-blue-500 text-white'
387
+ : hasContent
388
+ ? 'bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-blue-600 dark:hover:border-blue-500'
389
+ : 'bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-900 dark:text-white opacity-30 cursor-not-allowed'
390
+ }`}
391
+ disabled={!hasContent}
392
+ onClick={() => loadLetterPage(letter, 1)}
393
+ >
394
+ {letter}
395
+ </button>
396
+ );
397
+ })}
398
+ </div>
399
+ </div>
400
+
401
+ {/* Songs List */}
402
+ <div className="max-h-[500px] overflow-y-auto">
403
+ {songs.length === 0 ? (
404
+ <div className="text-center py-16 px-5 text-gray-500 dark:text-gray-500">
405
+ <div className="material-icons text-6xl mb-5 opacity-30">library_music</div>
406
+ <div>No songs found</div>
407
+ </div>
408
+ ) : (
409
+ songs.map((song) => (
410
+ <div
411
+ key={song.path}
412
+ className="flex justify-between items-start p-4 bg-gray-100 dark:bg-gray-700 rounded-md mb-2 transition-colors gap-4"
413
+ >
414
+ <div className="flex-1 flex flex-col items-start">
415
+ <div className="flex items-baseline gap-3 flex-wrap">
416
+ <div className="font-semibold text-base text-gray-900 dark:text-white inline-flex items-center gap-2">
417
+ {getFormatIcon(song.format)} {song.title}
418
+ </div>
419
+ <div className="text-[0.95rem] text-gray-600 dark:text-gray-400">
420
+ {song.artist}
421
+ </div>
422
+ </div>
423
+ {(song.album || song.year || song.genre) && (
424
+ <div className="text-[0.85rem] text-gray-500 dark:text-gray-500 mt-1">
425
+ {song.album && <span>{song.album}</span>}
426
+ {song.year && (
427
+ <span className="before:content-['_•_'] before:text-gray-400 dark:before:text-gray-600">
428
+ {song.year}
429
+ </span>
430
+ )}
431
+ {song.genre && (
432
+ <span className="before:content-['_•_'] before:text-gray-400 dark:before:text-gray-600">
433
+ {song.genre}
434
+ </span>
435
+ )}
436
+ </div>
437
+ )}
438
+ </div>
439
+ <div className="flex items-start gap-4 flex-shrink-0 pt-0.5">
440
+ <span className="text-gray-500 dark:text-gray-400 text-sm font-medium min-w-[45px] text-right">
441
+ {formatDuration(song.duration)}
442
+ </span>
443
+ <button
444
+ className="px-5 py-2.5 bg-blue-600 dark:bg-blue-500 border-none rounded-md text-white cursor-pointer flex items-center gap-1.5 font-medium text-[0.95rem] transition-all whitespace-nowrap hover:bg-blue-700 dark:hover:bg-blue-600 hover:-translate-y-px disabled:bg-gray-500 disabled:cursor-not-allowed disabled:opacity-50"
445
+ disabled={!allowRequests}
446
+ onClick={() => handleRequestSong(song)}
447
+ >
448
+ <span className="material-icons" style={{ fontSize: 18 }}>
449
+ add
450
+ </span>
451
+ Request
452
+ </button>
453
+ </div>
454
+ </div>
455
+ ))
456
+ )}
457
+ </div>
458
+
459
+ {/* Page Navigation */}
460
+ {totalPages > 1 && (
461
+ <div className="flex justify-center items-center gap-2 mt-4 p-3 bg-gray-100 dark:bg-gray-700 rounded-md flex-wrap">
462
+ <button
463
+ className="px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-white cursor-pointer text-sm transition-colors hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-blue-600 dark:hover:border-blue-500 disabled:opacity-40 disabled:cursor-not-allowed"
464
+ onClick={() => loadLetterPage(currentLetter, currentPage - 1)}
465
+ disabled={currentPage === 1}
466
+ >
467
+ Previous
468
+ </button>
469
+ <div className="flex gap-1 items-center">
470
+ {getPageNumbers().map((page, index) => {
471
+ if (page === '...') {
472
+ return (
473
+ <span
474
+ key={`ellipsis-${index}`}
475
+ className="px-2 text-gray-500 dark:text-gray-400 text-sm"
476
+ >
477
+ ...
478
+ </span>
479
+ );
480
+ }
481
+ return (
482
+ <button
483
+ key={page}
484
+ className={`px-3 py-2 border rounded cursor-pointer text-sm min-w-[40px] transition-all ${
485
+ page === currentPage
486
+ ? 'bg-blue-600 dark:bg-blue-500 border-blue-600 dark:border-blue-500 font-semibold text-white'
487
+ : 'bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-blue-600 dark:hover:border-blue-500'
488
+ }`}
489
+ onClick={() => loadLetterPage(currentLetter, page)}
490
+ >
491
+ {page}
492
+ </button>
493
+ );
494
+ })}
495
+ </div>
496
+ <span className="text-gray-500 dark:text-gray-400 text-[0.85rem] ml-2">
497
+ ({songs.length} songs)
498
+ </span>
499
+ <button
500
+ className="px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-white cursor-pointer text-sm transition-colors hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-blue-600 dark:hover:border-blue-500 disabled:opacity-40 disabled:cursor-not-allowed"
501
+ onClick={() => loadLetterPage(currentLetter, currentPage + 1)}
502
+ disabled={currentPage === totalPages}
503
+ >
504
+ Next
505
+ </button>
506
+ </div>
507
+ )}
508
+ </div>
509
+
510
+ {/* Queue Section */}
511
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-5 border border-gray-200 dark:border-gray-700">
512
+ <div className="text-xl font-semibold mb-4 flex items-center gap-2 text-gray-900 dark:text-white">
513
+ <span className="material-icons">queue_music</span>
514
+ Queue ({queue.length})
515
+ </div>
516
+ <ul className="list-none p-0 m-0">
517
+ {queue.length === 0 ? (
518
+ <div className="text-center py-16 px-5 text-gray-500 dark:text-gray-500">
519
+ <div className="material-icons text-6xl mb-5 opacity-30">queue_music</div>
520
+ <div>Queue is empty</div>
521
+ </div>
522
+ ) : (
523
+ queue.map((item, index) => (
524
+ <li
525
+ key={item.id}
526
+ className="flex justify-between items-center p-3 bg-gray-100 dark:bg-gray-700 rounded-md mb-2"
527
+ >
528
+ <div className="flex items-center">
529
+ <div className="w-8 h-8 flex items-center justify-center bg-blue-600 dark:bg-blue-500 rounded-full font-semibold mr-3 text-white">
530
+ {index + 1}
531
+ </div>
532
+ <div className="flex-1">
533
+ <div className="font-medium mb-1 text-gray-900 dark:text-white">
534
+ {item.title}
535
+ </div>
536
+ <div className="text-sm text-gray-500 dark:text-gray-400">
537
+ {item.artist} • Singer: {item.requester}
538
+ </div>
539
+ </div>
540
+ </div>
541
+ <span className="text-gray-500 dark:text-gray-400 text-sm">
542
+ {formatDuration(item.duration)}
543
+ </span>
544
+ </li>
545
+ ))
546
+ )}
547
+ </ul>
548
+ </div>
549
+
550
+ {/* Footer */}
551
+ <footer className="mt-8 py-6 text-center">
552
+ <a
553
+ href="https://loukai.app"
554
+ target="_blank"
555
+ rel="noopener noreferrer"
556
+ className="inline-flex items-center gap-2 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors text-sm"
557
+ >
558
+ <img src="/static/loukai-logo.png" alt="Loukai" className="w-6 h-6 rounded" />
559
+ <span>Powered by Loukai</span>
560
+ </a>
561
+ </footer>
562
+ </div>
563
+
564
+ {/* Request Modal */}
565
+ {showRequestModal && selectedSong && (
566
+ <div
567
+ className="fixed top-0 left-0 w-full h-full bg-black/80 z-[1000] flex items-center justify-center"
568
+ onClick={() => setShowRequestModal(false)}
569
+ >
570
+ <div
571
+ className="bg-white dark:bg-gray-800 rounded-lg p-8 max-w-lg w-[90%] border border-gray-200 dark:border-gray-700"
572
+ onClick={(e) => e.stopPropagation()}
573
+ >
574
+ <div className="mb-5">
575
+ <div className="text-2xl mb-2 text-gray-900 dark:text-white">Request Song</div>
576
+ <div className="text-gray-900 dark:text-white text-xl font-bold mb-2 p-2.5 bg-gray-100 dark:bg-gray-700 rounded-md border-l-4 border-blue-600 dark:border-blue-500">
577
+ {selectedSong.title} - {selectedSong.artist}
578
+ </div>
579
+ </div>
580
+
581
+ <div className="mb-5">
582
+ <label className="block mb-2 text-gray-600 dark:text-gray-300 text-sm">
583
+ Your Name
584
+ </label>
585
+ <div className="px-3 py-2 bg-gray-100 dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white font-medium">
586
+ {userName}
587
+ </div>
588
+ </div>
589
+
590
+ <div className="mb-5">
591
+ <label className="block mb-2 text-gray-600 dark:text-gray-300 text-sm">
592
+ Message (optional)
593
+ </label>
594
+ <textarea
595
+ className="w-full px-2.5 py-2.5 bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-white text-base resize-y min-h-[80px] focus:outline-none focus:border-blue-600 dark:focus:border-blue-500"
596
+ placeholder="Any special requests or notes..."
597
+ value={requestMessage}
598
+ onChange={(e) => setRequestMessage(e.target.value)}
599
+ maxLength={200}
600
+ />
601
+ </div>
602
+
603
+ <div className="flex gap-2.5 justify-end">
604
+ <button className="btn btn-secondary" onClick={() => setShowRequestModal(false)}>
605
+ Cancel
606
+ </button>
607
+ <button className="btn btn-primary" onClick={submitRequest}>
608
+ Submit Request
609
+ </button>
610
+ </div>
611
+ </div>
612
+ </div>
613
+ )}
614
+
615
+ {/* Toast notifications */}
616
+ {toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
617
+ </div>
618
+ );
619
+ }
@@ -0,0 +1,68 @@
1
+ /* Tailwind CSS directives */
2
+ @tailwind base;
3
+ @tailwind components;
4
+ @tailwind utilities;
5
+
6
+ /* Custom layer for web-specific components */
7
+ @layer components {
8
+ /* Button base styles */
9
+ .btn {
10
+ @apply px-4 py-2 rounded-md font-medium transition-all duration-200;
11
+ @apply bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-600;
12
+ @apply hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed;
13
+ }
14
+
15
+ .btn-primary {
16
+ @apply bg-blue-600 dark:bg-blue-500 border-blue-600 dark:border-blue-500 text-white;
17
+ @apply hover:bg-blue-700 dark:hover:bg-blue-600;
18
+ }
19
+
20
+ .btn-secondary {
21
+ @apply bg-transparent text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700;
22
+ @apply hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white;
23
+ }
24
+
25
+ .btn-sm {
26
+ @apply px-3 py-1.5 text-sm;
27
+ }
28
+
29
+ .btn-xs {
30
+ @apply px-2 py-0.5 text-xs min-h-[24px];
31
+ }
32
+
33
+ /* Input styles */
34
+ .input {
35
+ @apply w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md;
36
+ @apply text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-500 transition-colors;
37
+ @apply focus:outline-none focus:border-blue-600 dark:focus:border-blue-500 focus:ring-1 focus:ring-blue-600 dark:focus:ring-blue-500;
38
+ }
39
+
40
+ /* Card styles */
41
+ .card {
42
+ @apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-5;
43
+ }
44
+
45
+ /* Badge styles */
46
+ .badge {
47
+ @apply inline-flex items-center justify-center min-w-[20px] h-5 px-1.5;
48
+ @apply bg-red-600 text-white rounded-full text-[11px] font-semibold leading-none;
49
+ }
50
+ }
51
+
52
+ /* Dark mode base styles */
53
+ body {
54
+ @apply bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white;
55
+ }
56
+
57
+ /* Custom scrollbar styles */
58
+ ::-webkit-scrollbar {
59
+ @apply w-2 h-2;
60
+ }
61
+
62
+ ::-webkit-scrollbar-track {
63
+ @apply bg-gray-100 dark:bg-gray-800 rounded;
64
+ }
65
+
66
+ ::-webkit-scrollbar-thumb {
67
+ @apply bg-gray-300 dark:bg-gray-600 rounded hover:bg-gray-400 dark:hover:bg-gray-500;
68
+ }