From 0b462e4655eaa99a1b65d77df80bbba61f6ac8ab Mon Sep 17 00:00:00 2001 From: Hoang Duc Quan Date: Mon, 19 Aug 2024 09:23:36 +0700 Subject: [PATCH 1/2] Use React JS (#51) * update yarn lock * install React * add demo react * change into React Metronome Button * Metronome using ReactJS --- app/javascript/application.js | 1 + app/javascript/components/MetronomeButton.tsx | 35 ++++++++++ app/javascript/components/index.tsx | 32 +++++++++ app/javascript/controllers/index.js | 2 - .../controllers/metronome_controller.js | 34 --------- app/views/songs/show.html.slim | 6 +- package.json | 7 +- tsconfig.json | 10 +++ yarn.lock | 70 ++++++++++++++++++- 9 files changed, 152 insertions(+), 45 deletions(-) create mode 100644 app/javascript/components/MetronomeButton.tsx create mode 100644 app/javascript/components/index.tsx delete mode 100644 app/javascript/controllers/metronome_controller.js create mode 100644 tsconfig.json diff --git a/app/javascript/application.js b/app/javascript/application.js index 1196aa7..20e00ba 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,4 +2,5 @@ //= require app_assets import "@hotwired/turbo-rails" import "./controllers" +import "./components" import * as bootstrap from "bootstrap" diff --git a/app/javascript/components/MetronomeButton.tsx b/app/javascript/components/MetronomeButton.tsx new file mode 100644 index 0000000..495d311 --- /dev/null +++ b/app/javascript/components/MetronomeButton.tsx @@ -0,0 +1,35 @@ +import React, { useState, useRef, useEffect } from 'react'; + +interface MetronomeProps { + tempo: number; + sound: string; +} + +const MetronomeButton = ({ tempo, sound }: MetronomeProps) => { + const [isPlaying, setIsPlaying] = useState(false); + const timerRef = useRef(0); + const beat = new Audio(sound); + + const handleClick = () => { + setIsPlaying(!isPlaying); + }; + + useEffect(() => { + if (isPlaying) { + timerRef.current = setInterval(() => { + beat.play(); + }, (60 / tempo) * 1000); + } else { + clearInterval(timerRef.current); + } + return () => clearInterval(timerRef.current); + }, [isPlaying, tempo]); + + return ( + + ); +} + +export default MetronomeButton diff --git a/app/javascript/components/index.tsx b/app/javascript/components/index.tsx new file mode 100644 index 0000000..6b353d8 --- /dev/null +++ b/app/javascript/components/index.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import MetronomeButton from "./MetronomeButton"; + +const metronome_container = document.getElementById("metronome-button"); + +if (!metronome_container) { + throw new Error("Couldn't find metronome element"); +} + +// Handle Metronome action +const metronome_root = createRoot(metronome_container); +const tempo = metronome_container.getAttribute("data-tempo") +const sound = metronome_container.getAttribute("data-sound") + +if (!tempo) { + throw new Error("Couldn't find tempo"); +} + +if (!sound) { + throw new Error("Couldn't find sound"); +} + +const parsedTempo = parseInt(tempo, 10); + +document.addEventListener("DOMContentLoaded", () => { + metronome_root.render( + <> + + + ); +}); \ No newline at end of file diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 4d75af8..66d34e1 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -5,6 +5,4 @@ import { application } from './application' import ArtistsSongsController from './artists_songs_controller' -import MetronomeController from './metronome_controller' application.register('artists-songs', ArtistsSongsController) -application.register('metronome', MetronomeController) diff --git a/app/javascript/controllers/metronome_controller.js b/app/javascript/controllers/metronome_controller.js deleted file mode 100644 index fba6448..0000000 --- a/app/javascript/controllers/metronome_controller.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Controller } from '@hotwired/stimulus' - -export default class MetronomeController extends Controller { - static targets = ['bpm', 'tempoButton', 'sound'] - - connect() { - this.bpm = parseInt(this.bpmTarget.value) - this.playing = false - this.tempoButtonTarget.textContent = this.tempoButtonTarget.innerHTML - this.sound = this.soundTarget.value - this.beat = new Audio(this.sound) - } - - handleClick() { - this.playing = !this.playing - - if (this.playing) { - this.start() - } else { - this.stop() - } - } - - start() { - console.log(this.bpm) - this.timer = setInterval(() => this.beat.play(), (60 / this.bpm) * 1000) - this.tempoButtonTarget.textContent = 'Stop metronome for the song' - } - - stop() { - clearInterval(this.timer) - this.tempoButtonTarget.textContent = 'Start metronome for the song' - } -} diff --git a/app/views/songs/show.html.slim b/app/views/songs/show.html.slim index 53e9e4b..ef6ed65 100644 --- a/app/views/songs/show.html.slim +++ b/app/views/songs/show.html.slim @@ -26,12 +26,8 @@ p strong Tempo: = @song.tempo.to_s + ' BPM' + = tag.div id: 'metronome-button', data: {tempo: @song.tempo, sound: audio_path('drumsticks.mp3') } - div[data-controller='metronome'] - input[type="hidden" data-metronome-target="bpm" value="#{@song.tempo}"] - input[type="hidden" data-metronome-target="sound" value="#{audio_path('drumsticks.mp3')}"] - = button_tag '', type: 'button', data: { action: 'metronome#handleClick' }, class: 'btn btn-primary' - span[data-metronome-target='tempoButton'] Start metronome for the song div strong Lyric: =< simple_format(@song.lyric) diff --git a/package.json b/package.json index e490a6e..8650a4d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "@hotwired/stimulus": "^3.2.1", "@hotwired/turbo-rails": "^7.3.0", "@popperjs/core": "^2.11.8", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.14", "bootstrap": "^5.3.0", "bootstrap-icons": "^1.10.5", @@ -12,7 +14,10 @@ "nodemon": "^2.0.22", "postcss": "^8.4.24", "postcss-cli": "^10.1.0", - "sass": "^1.63.6" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sass": "^1.63.6", + "typescript": "^5.5.4" }, "scripts": { "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..da83dbb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "jsx": "react", + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bed12db..f6a11f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -161,6 +161,26 @@ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.5.tgz#bbc11203e0d3d5084002abfcf01d621fdf5f3a9d" integrity sha512-SOBA2heB9lTw0VYIx8M/ed7inSf4I9sR8OIlJprhgkfQ3WJtrxPJ6DDATR1Z3RYaIR7HlT2Olj08v1lfGIGuHA== +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + +"@types/react-dom@^18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.3.3": + version "18.3.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" + integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -244,9 +264,9 @@ browserslist@^4.21.5: update-browserslist-db "^1.0.11" caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503: - version "1.0.30001507" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001507.tgz#fae53f6286e7564783eadea9b447819410a59534" - integrity sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A== + version "1.0.30001651" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" + integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== "chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.5.2: version "3.5.3" @@ -289,6 +309,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -477,6 +502,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -491,6 +521,13 @@ lilconfig@^2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -642,6 +679,21 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -682,6 +734,13 @@ sass@^1.63.6: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -756,6 +815,11 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" From e0b3dfdc1eab9eba8c5637eb5895334f4fbc20e6 Mon Sep 17 00:00:00 2001 From: Hoang Duc Quan Date: Mon, 19 Aug 2024 09:24:59 +0700 Subject: [PATCH 2/2] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index fd67e3e..f71d3bd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ - PostgreSQL - Ruby 3.1 -> Ruby 3.3 - Rails 7.0 -> Rails 7.2 +- React 18.3.1 - RSpec for test - Coding Convention, Code Coverage @@ -13,6 +14,8 @@ Jun 25, 2023: Init project. Ruby 3.1, Rails 7.0 Aug 12, 2024: Ruby 3.3, Rails 7.2 +Aug 19, 2024: Ruby 3.3, Rails 7.2, React 18.3.1 + ## Why ### The idea The idea is a lyrics that show the lyrics and the tempo of the song.