Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Hotwire or React? ~Reactの録画機能をHotwireに置き換えて得られた...
Search
haruna tsujita
October 25, 2024
Programming
11k
11
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Hotwire or React? ~Reactの録画機能をHotwireに置き換えて得られた知見~ / hotwire_or_react
haruna tsujita
October 25, 2024
More Decks by haruna tsujita
See All by haruna tsujita
Rebuilding Turbo Streams with ruby.wasm and Ruby Sockets
harunatsujita
1
2.3k
Hotwire or React? ~アフタートーク・本編に含めなかった話~ / Hotwire or React? after talk
harunatsujita
1
310
fbc-graduation-napple
harunatsujita
0
130
Rails Girls 2022 LT
harunatsujita
0
660
はじめてのしくじり /fjordbootcamp-211009
harunatsujita
0
1.2k
Other Decks in Programming
See All in Programming
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
190
AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す
theoriatec2024
1
500
The Rails Doctrine Decade
koic
2
150
[GoCon2026] When Goroutines Are Not Enough: Runtime Locality in High-Throughput Go
takehaya
6
2.3k
AGENTS.md Is Not Enough:Build Skills, Don't Download Them
lx_t
0
120
SONY CISC-NEWS NWS-1750 + NWB-225 フレームバッファの NetBSD/news68k ドライバ実装 / OSC2026Hiroshima
tsutsui
0
120
AI活用は、個人から組織へ|マルチプレイヤーエージェントハーネス「QM」の社内活用事例 / AI use is moving from individuals to orgs
rkaga
1
120
大喜利で理解するLLM as a Judge / Understanding LLM-as-a-Judge through Ogiri
rockname
0
150
モバイル交通系ICへのチャージ実例から考える、クロスプラットフォーム開発におけるiOS実機テスト設計とCI運用
yusuga
1
520
Vibes Containers 〜AIで変わるコンテナ設計と運用〜
tkikuc
3
550
ゲームコントローラやキーボードのファームウェアをSwiftで書く
kishikawakatsumi
1
250
フロントエンドUIフレームワークのこれまでとこれから
ssssota
5
2.8k
Featured
See All Featured
Unsuck your backbone
ammeep
672
58k
Docker and Python
trallard
47
4.2k
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
330
The Cost Of JavaScript in 2023
addyosmani
55
10k
[SF Ruby Conf 2025] Rails X
palkan
3
1.4k
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.2k
The SEO identity crisis: Don't let AI make you average
varn
0
560
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
6.1k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
240
Are puppies a ranking factor?
jonoalderson
2
3.9k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
3k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.8k
Transcript
)BSVOB5TVKJUB !IBSVOBUTVKJUB ,BJHJPO3BJMT )PUXJSFPS3FBDU d3FBDUͷըػೳΛ)PUXJSFʹஔ͖͑ͯಘΒΕͨݟd
ࣗݾհ !IBSVOBUTVKJUB ✦ˠגࣜձࣾΩϟλϧʢӳޠक़ΛӡӦʣ ✦όοΫΤϯυΤϯδχΞʢ3BJMTͱɺͨ·ʹ3FBDUʣ ✦ϥΠςΟϯάͷఴαʔϏεΛ։ൃ
ࠓͷςʔϚ ✦ϝΠϯ)PUXJSFɺಛʹ4UJNVMVTͷͰ͢ ✦ٕज़બఆʹؔ͢Δ͠·͢ w ࢲ3FBDU͖Ͱ͢ w ຊͷલʹϓϩμΫτνʔϜͷঢ়گΛ؆୯ʹڞ༗͠·͢
ϥΠςΟϯάͷఴαʔϏεͱʁ w ӳޠक़Ωϟλϧʹ௨͏ੜెʹఏڙ͍ͯ͠ΔখنͳΞϓϦέʔγϣϯ w 3VCZPO3BJMT෦తʹ3FBDUͰ41"Λ࣮ݱ ڭࢣ ੜె ӳޠͰϥΠςΟϯάΛॻ͘ɾఏग़ ఴɾϑΟʔυόοΫ
3FBDUɺຊʹඞཁʁ ͜ͷ͘Β͍ͷنͷαʔϏεʹ
w 3BJMTͷํ͕ಘҙͳόοΫΤϯυΤϯδχΞ໊͕։ൃ w ޠΧϯτɾςΩετΤϦΞͷϋΠϥΠτͳͲͪΐͬͱͨ͠ՕॴΛ41"ʹ͍ͨ͠ ‣3FBDUͰॻ͔ΕͨՕॴ͕ঃʑʹෛ࠴Խ ‣ϝΠϯͷػೳ։ൃʹ͕͔͔࣌ؒͬͯ͠·͏ w Γ͍ͨ͜ͱ)PUXJSFͰे࣮ݱͰ͖ΔͷͰʁ 3FBDUؔ࿈ͷ՝
.ZOBNFJT)BSVOB *`NGSPN5PLZP େֻ͔Γͳ͜ͱ͍ͯ͠ͳ͍ʝ
3FBDUΛΊͯ)PUXJSFʹ͢Δͷා͍ʂʂʂ
ා͞ͷਖ਼ମʢͱࢥΘΕΔͷʣ 3FBDU ΤίγεςϜݟ͕ॆ࣮ʢͱ͘ʹ$IBLSB6*Λ͍͍ͨʣ ॊೈʹ6*Λ࣮Ͱ͖Δ )PUXJSF 5VSCPͷΠϝʔδɾ$36%ૢ࡞Λ/P+4Ͱ41"ϥΠΫʹͰ͖Δ ཧػೳ͘Β͍ͷϓϩμΫτʹϑΟοτʁ 4UJNVMVTͷใ͕͋·Γͳͯ͘ະ
)PUXJSFͷΈͰे։ൃͰ͖Δʁ3FBDUΛख์ͯ͠ຊʹ͍͍ͷʁ
ٕज़ݕূ͢ΔՁ͋Γͦ͏ʂ w ݒ೦ͷҰͭ4UJNVMVT ‣͜Ε͕ϑΟοτ͠ͳ͚Ε)PUXJSFஔ͖͑ͷબࢶ͕ফ͑Δ w 3FBDUΛؤுΖ͏ʂͱܾҙͰ͖ΔͷνʔϜʹͱͬͯϓϥεͳͣ
ըػೳΛஔ͖͑ͯΈΔ w ϑΟʔυόοΫಈըͷըػೳ ‣ಈըσʔλͷอଘɾ࠶ੜɾআ ‣1$ͷΧϝϥɾϚΠΫͷૢ࡞͕ೖΔͨΊ ࣮֬ʹ4UJNVMVTΛ͏ ‣ػೳվળ͕׆ൃʹߦΘΕͳ͍Օॴ 3FDPSE $MFBS
ஔ͖͑ͷલఏ݅ w 3FBDUͱ)PUXJSFͷҧ͍Λݕূ͢Δ͜ͱ͕త ‣طଘͷ6*69Λҡ࣋͠ɺ༷มߋ͠ͳ͍
3FDPSE $MFBS
3FDPSE $MFBS 4UPQ $MFBS )J 3FDPSEˠ4UPQ ըελʔτ
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
ಈը࠶ੜ EJTBCMFEղআ EJTBCMFEUSVF 1045
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
1045 %&-&5&
3FBDUͰͷ࣮Λݟ͍͖ͯ·͠ΐ͏
ݱঢ়ͷ࣮ 3FBDU ఴϖʔδ 3FDPSE $MFBS // app/views/summaries/feedbacks/edit.html.haml -# লུ #summary-video-recorder{data:
{'summary-id': @summary.id}} = javascript_include_tag ‘SummaryVideoRecorder' -# লུ w ఴϖʔδͷWJFX͔Β3FBDUΛݺͼग़͢
w ಈըͷදࣔɾ3FDPSEϘλϯɾ$MFBSϘλϯ͕ίϯϙʔωϯτԽ w ϘλϯͷΫϦοΫΠϕϯτΛىʹॲཧ͕࣮ߦ͞ΕΔ return ( <> <Video theme='large' videoUrl={videoUrl}
mimeType='video/webm' /> <p> <RecordButton startRecording={startRecording} stopRecording={stopRecording} id={summaryId} videoUrl={videoUrl} /> <ClearButton onClick={handleClear} isDisabled={isPosting || !videoUrl} /> </p> </> ) } ݱঢ়ͷ࣮ 3FDPSE $MFBS
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL 5VSCPͱ૬ੑͷΑ͍ +BWB4DSJQUϑϨʔϜϫʔΫ
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL ΠϕϯτͰൃੜ͢ΔΞΫγϣϯΛࢦఆ
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL )BSVOBͱೖྗͯ͠(SFFUϘλϯΛԡ͢ͱz)FMMP )BSVOBz͕ग़ྗ
)PUXJSFஔ͖͑ // app/javascript/controllers/video_recorder_controller.js export default class extends Controller { static
targets = ["video"] static values = { summaryId: Number } } -# app/views/summaries/feedbacks/edit.html.haml %div{data: {controller: "video-recorder", video_recorder_summary_id_value: @summary.id}} %video{data: {video_recorder_target: "video"}, controls: true, autoplay: true, muted: true} %button{data: {action: "click->video-recorder#startRecording"}} Record %button{data: {action: "click->video-recorder#stopRecording"} Stop %button{data: {action: "click->video-recorder#clearVideo"} Clear
)PUXJSFஔ͖͑ ్࣮தͳͷʹɺͳΜ͔+BWB4DSJQU ͍ͬͺ͍ॻ͍ͯΔͧɾɾʁ // app/javascript/controllers/video_recorder_controller.js import { Controller } from
"@hotwired/stimulus" // Connects to data-controller="feedback-video" export default class extends Controller { static targets = ["video", "recordButton", "stopButton", "clearButton"] static values = { summaryId: Number } async connect() { console.log(this.summaryIdValue) this.chunks = [] const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) this.videoTarget.srcObject = stream this.videoTarget.play() this.mediaRecorder = new MediaRecorder(stream) this.mediaRecorder.ondataavailable = (event) => this.chunks.push(event.data) } startRecording() { try { console.log('start recording') this.mediaRecorder.start() } catch(error) { console.error('Error accessing media devices.', error) } } stopRecording() { console.log('stop recording') this.mediaRecorder.stop() this.mediaRecorder.onstop = this.handleStop.bind(this) } handleStop() { const blob = new Blob(this.chunks, { type: 'video/webm' }) const url = URL.createObjectURL(blob) this.videoTarget.src = url this.uploadVideo(blob) } async uploadVideo(blob) { const formData = new FormData() formData.append( 'summary_video[video]', blob, `summary_video_id${this.summaryIdValue}.webm` ) const response = await fetch(`/api/summaries/${this.summaryIdValue}/summary_feedback_videos`, { method: 'POST', body: formData, headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }) if (response.ok) { await response.json() console.log('Video uploaded') } else { console.error('Failed to upload video', await response.text()) } } clearVideo() { try { fetch(`/api/summaries/${this.summaryIdValue}/summary_feedback_videos`, { method: 'DELETE', headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }) } catch(error) { console.error('Error deleting video', error) } } } ˞தಡ·ͳͯ͘େৎͳͷͰɺ ࠞಱͱͨ͠ײ͡ΛΈऔ͍͚ͬͯͨͩΔͱخ͍͠Ͱ͢ɻ
4UJNVMVTΛ৮ͬͯΈͨॴײ w ಈ࡞࣮ݱͰ͖ͦ͏ɻ w 4UJNVMVTࣗମʹॻ͖ͮΒ͞ͳ͍ɻ w Ͱɺͬ͘͠Γ͜ͳ͍ɻ ͱɺࢥ͍ͭͭνʔϜϝϯόʔʹ్தܦաΛڞ༗ͨ͠ͱ͜Ζɾɾɾ
ΰϦΰϦʹ+BWB4DSJQUॻ͘͜ͱʹͳͬͪΌ͏Ͷ ͜ΕͳΒ3FBDUͰྑ͍ͷͰʁ
ϓϩϙʔβϧఏग़࣌͜͏݁Λग़͍ͯͨ͠
ຊʹ͜ΕͰɺྑ͍ͷ͔ɾɾʁʁʁ
ʢ࠶ܝʣ4UJNVMVTΛ৮ͬͯΈͨॴײ w ಈ࡞࣮ݱͰ͖ͦ͏ɻ w 4UJNVMVTࣗମʹॻ͖ͮΒ͞ͳ͍ɻˠ w Ͱɺͬ͘͠Γ͜ͳ͍ɻ ͱɺࢥ͍ͭͭνʔϜϝϯόʔʹ్தܦաΛڞ༗ͨ͠ͱ͜Ζɾɾɾ
w 3FBDUʹؔ͢ΔࣝʢVTF4UBUFͳͲͷ)PPLTʣ͕ඞཁͳ͍ ‣)PPLT͕Α͠ͳʹॲཧ͍ͯͨ͠ՕॴΛࣗͰ࣮͢Δ͜ͱʹ w ָʹϑϩϯτΤϯυΛॻ͖͔ͨͬͨͣͳͷʹ 3FBDUΛ༻͢ΔΑΓ+BWB4DSJQUͷίʔυ͕૿Ճ ॻ͖ͮΒ͕͞ͳ͍ɺͷਖ਼ମʁ
վળͷ༨Λ୳Δ w 4UJNVMVTίϯτϩʔϥʔʹաͳ+BWB4DSJQUͷϩδοΫ͕ूத ‣ͦΕͳΒ3FBDU͕ྑ͍ w ͔͠͠ɺผͷՕॴͰ5VSCPΛ࣮ͬͯͨ͠ͱ͖ ‣5VSCPνʔϜͰධͩͬͨ
࣮ํΛݟ͢ w Ծઆ̍ɿ)PUXJSFͷࢫΈ5VSCPͳͷͰ ‣5VSCPʹدͤΔ͜ͱͰɺ4UJNVMVTʹॻ͘+BWB4DSJQU͕ݮΔͣ w Ծઆ̎ɿ4UJNVMVTͷίϯτϩʔϥʔΛཧ͢Δͱҹ͕ҧ͏ͷͰ
)PUXJSF5VSCPͱʁ w $36%ૢ࡞Λத৺ͱͨ͠41"ϥΠΫͳମݧΛ+BWB4DSJQUΛॻ͔ͣʹ ࣮ݱͰ͖Δπʔϧ ങ͍ʹߦ͘ ݘʹ㕒Λ͋͛Δ
)PUXJSF5VSCPͱʁ w ϦΫΤετʹରͯ͠ɺαʔόʔࢦఆͨ͠Օॴͷ)5.-ΛϨεϙϯε ‣ը໘શମΛ࠶ඳը͠ͳ͍ͷͰ41"ϥΠΫͳಈ࡞Λ࣮ݱͰ͖Δ ങ͍ʹߦ͘ ݘʹ㕒Λ͋͛Δ
ըػೳͷͲ͜Ͱ5VSCPΛ͑Δʁ
˞ҎԼ,BJHJPO3BJMTαΠτ͔ΒҰ෦ൈਮ 41"ͱ)PUXJSFͰɺΞϓϦΛߏ͢ΔͨΊͷͷߟ͑ํ͕େ͖͘ҟͳΓ·͢ɻ )PUXJSFΛॎԣແਚʹ͍ͨ͢Ίʹɺ)PUXJSFతͳߟ͑ํͰ࡞Δඞཁ͕͋ΔͷͰ͢ɻ ͜ͷɺ)PUXJSFతͳߟ͑ํʹઃܭࢦʹࢲʮ8FCࢴࣳډʯͱ͍͏໊લΛ͚ͭ·ͨ͠ɻ
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
1045 %&-&5& ը໘ͷྲྀΕʹԊ࣮ͬͯ͢Δͷμϝͩͬͨɾɾ
ߟ͑ํΛม͑ͯΈΔʂ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ʢ࠶ܝʣϘλϯͷΫϦοΫͰঢ়ଶ͕ભҠ͢Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& %#ʹಈը σʔλͳ͠ %#ʹಈը σʔλ͋Γ ը։࢝ ऴྃ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 5VSCPΛར༻Ͱ͖ͦ͏
ࢴࣳډͱͯ͠ߟ͑ͯΈΔ 3FDPSE ᶃಈըσʔλͳ͠ $MFBS ᶄಈըσʔλ͋Γ 1045 %&-&5& $MFBS 3FDPSE w
$36%ૢ࡞͕͖͔͚ͬͰൃੜ͢Δը໘ͷΓସ͑5VSCPʹͤΔ ˞ʰ8FCࢴࣳډʱͷτʔΫը໘શମΛຕͷࢴࣳډͱͯ͠ଊ͑Δ͓ͳͷͰɺ τʔΫ͔ΒώϯτΛಘͨ͏͑ͰҟͳΔΞϓϩʔνΛ͍ͯ͠·͢
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ ʁʁʁ ը։࢝ऴྃ Ϙλϯͷ%0.ૢ࡞ͳͲ
ࢴࣳډͱͯ͠ߟ͑ͯΈΔᶄ 4UPQ ᶃ $MFBS 3FDPSE w lࢴࣳډͷֻ͚zͱଊ͑ͯΈΔ ‣3BJMTଆͰ੍ޚ͢Δͷ͍͠ ‣4UJNVMVTͰ࣮͢Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 5VSCPͰ࣮
)PUXJSFஔ͖͑d5VSCPΛ͏d // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder",
{//stimulusίϯτϩʔϥσʔλ͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ // ఴϖʔδͷview = render partial: 'summaries/feedbacks/summary_feedback_video', locals: { // লུ } ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX
// app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {//
ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? %video %source{src: summary_feedback_video.video_url, type: "video/webm"} = link_to 'Clear', summary_summary_feedback_videos_path(summary), data: { turbo_confirm: "ຊʹআ͠·͔͢ʁ", turbo_method: :delete } - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ ˞εϖʔεͷؔͰࢿྉͰҰ෦লུ͍ͯ͠ΔՕॴ͕͋Γ·͢ # app/controllers/summaries/summary_feedback_videos_controller.rb def destroy @summary = Summary.find(params[:summary_id]) @summary_feedback_video = @summary.summary_feedback_video @summary_feedback_video.destroy! end ˢ҉తʹUVSCP@TUSFBNΛSFOEFS ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔ
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBN ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX ˞εϖʔεͷؔͰࢿྉͰվߦΛೖΕ͍ͯ·͢
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBN // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {// ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ ઌ΄Ͳ·Ͱσʔλ͕ ͋ͬͨͷͰͪ͜Β
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBNˠ // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {// ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ σʔλ͕আ͞Εͨ ͷͰͪ͜Β͕࣮ߦ͞ΕΔ ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX
3FDPSE ᶃಈըσʔλͳ͠ $MFBS ᶄಈըσʔλ͋Γ 1045 %&-&5& $MFBS 3FDPSE ʢ࠶ܝʣࢴࣳډͷجૅ͕ग़དྷ্͕Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 4UJNVMVTͰ࣮
ըʹؔ͢ΔॲཧτʔΫͷຊےͰͳ͍ͷͰɺ ίʔυΛཧͨ͠ํ๏ʹ͍͓ͭͯ͠͠·͢
ʢ࠶ܝʣ࣮ํΛݟ͢ w Ծઆ̍ɿ)PUXJSFͷࢫΈ5VSCPͳͷͰ ‣5VSCPʹدͤΔ͜ͱͰɺ4UJNVMVTʹॻ͘+BWB4DSJQU͕ݮΔͣ w Ծઆ̎ɿ4UJNVMVTͷίϯτϩʔϥʔΛཧ͢Δͱҹ͕ҧ͏ͷͰ ‣ϦϑΝΫλϦϯάํ๏·ͩࡧதͰ͕͢ɾɾ
// app/javascript/controllers/video_recorder_controller.js import { Controller } from "@hotwired/stimulus" import {
// ར༻͢Δؔ } from “./video_recorder_utils" export default class extends Controller { static targets = ["video", "recordButton"] static values = { writingId: Number, writingType: String, feedbackVideo: String, uploadUrl: String } async connect() { // ଓ࣌ͷॲཧ } async startRecording() { // ը։࢝ͷॲཧ } stopRecording() { // ըऴྃͷॲཧ } }
// app/javascript/controllers/video_recorder_controller.js import { Controller } from "@hotwired/stimulus" import {
initializeMediaStream, setupMediaRecorder, setStopButton, uploadData } from “./video_recorder_utils" // Connects to data-controller="video-recorder" export default class extends Controller { static targets = ["video", "recordButton"] static values = { writingId: Number, writingType: String, feedbackVideo: String, uploadUrl: String } async connect() { if (!this.feedbackVideoValue) { this.mediaStream = await initializeMediaStream() } } async startRecording() { if (!this.mediaStream) { this.mediaStream = await initializeMediaStream() if (!this.mediaStream) return } this.mediaRecorder = setupMediaRecorder(this.mediaStream) this.mediaRecorder.start() setStopButton(this.recordButtonTarget) this.mediaRecorder.ondataavailable = (event) => uploadData(event, this.writingTypeValue, this.writingIdValue, this.uploadUrlValue) } stopRecording() { this.mediaRecorder?.stop() this.mediaStream.getTracks().forEach(track => track.stop()) this.recordButtonTarget.disabled = true }} 5PUBMߦ
͜ΕͰஔ͖͑ྃͰ͢ʂ
13Ϛʔδ͞Ε·ͨ͠🎉 ٞͷ݁Ռʜ
13Ϛʔδ͞Ε·ͨ͠🎉 ˞ࠩ Ͱ͕͢ɺఴը໘ͷըػೳՕॴͷஔ͖͑Λ͓͜ͳ͍ɺ 3FBDUଆͰڞ௨Խ͞Ε͍ͯͳ͔ͬͨՕॴ͕ࠓճڞ௨Խ͞Εͯͷ݁ՌͳͷͰ Օॴͷ߹ɺݮগྔఔʹऩ·Γ·͢
)PUXJSFPS3FBDUʁ
$36%ͷΈ $36% +4 $36%ͳ͠ +4 )PUXJSFPS3FBDUʁ w $36%ૢ࡞͕ओମ ิॿతͳ+BWB4DSJQU͕ඞཁͳը໘ ‣ࢴࣳډ
ͪΐͬͱֻ͚͕ͨ͋͠Δ߹ w )PUXJSFͰेରԠͰ͖Δɺੵۃతʹ͍͖͍ͬͯͨ
w $36%ૢ࡞ΛΘͣϦονͳΠϯλϥΫγϣϯ͕ඞཁͳը໘ ‣FYෳࡶͳεςʔτཧ͕ඞཁ w ࢲͷ୲ϓϩμΫτʹ֘͠ͳ͍ ‣3FBDUͷಘҙ $36%ͷΈ $36% +4 $36%ͳ͠
+4 )PUXJSFPS3FBDUʁ
w $36%ΛؚΉߴͳ41"࣮͕ඞཁͳը໘ w $36%ͳ͠Ͱγϯϓϧͳ41"࣮͕ඞཁͳը໘ ‣୲ϓϩμΫτͰςΩετΤσΟλ͕֘ ‣͜ͷ͋ͨΓҙݟ͕͔ΕΔ )PUXJSFPS3FBDUʁ $36%ͷΈ $36% +4
$36%ͳ͠ +4
w $36%ͳ͠Ͱ41"࣮͕ඞཁͳը໘ ‣ଟ͘Ϣʔβʔ͕࣌ؒࡏ͢Δϖʔδ ‣ຕͷࢴࣳډͷதʹֻ͚͕ͨ͘͞Μ͋Δॴ )PUXJSFPS3FBDUʁ $36%ͷΈ $36% +4 $36%ͳ͠ +4
)PUXJSFPS3FBDUʁ $36%ͳ͠ͷ 41"࣮Λ͢Δ߹ ✦3FBDU w )PPLTΤίγεςϜͷਅՁΛൃش࢝͠ΊΔ ‣͔͠͠3FBDUʹର͢Δਂ͍ཧղ͕ඞཁ ✦)PUXJSF w αʔόʔαΠυʹ࣮ΛدͤΒΕͯѻ͍͍͢
‣࣮ͷɾ)PUXJSFతઃܭ ࣮Λ$36%ʹམͱ͠ࠐΉඞཁ͕͋Δ $36%ͷΈ $36% +4 $36%ͳ͠ +4
)PUXJSFPS3FBDUʁ w 3FBDUͱ)PUXJSFɺͲͪΒҰҰ͋Γɺબ͢Δͷ͍͠ w ͨͩ͠ɺʰ)PUXJSFͰ࣮Ͱ͖ͦ͏͔ʁʱͱ͍͏ࢹͩͱ w ࣮Λ$36%ʹूͰ͖Δ͔Ͳ͏͔ɺ͕େ͖ͳϙΠϯτ
✦)PUXJSFతͳઃܭ͕Ͱ͖Εɺద༻ՄೳͳྖҬ૾ΑΓ͍ ✦)PUXJSFΛͬͯΈΑ͏͔ͳɺͱࢥ͏ํ͕૿͑ͨΒخ͍͠Ͱ͢ʂ ·ͱΊ
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ
ࢀߟ w )PUXJSF)BOECPPLIUUQTIPUXJSFEEFW w 5VSCP)BOECPPLʢຊޠ༁ʣIUUQTFWFSZMFBGHJUIVCJP IPUXJSF@KBUVSCPIBOECPPLJOUSPEVDUJPO w ೣͰΘ͔Δ)PUXJSFೖ5VSCPฤIUUQT[FOOEFWTIJUB CPPLTDBUIPUXJSFUVSCP w
)PUXJSFతͳઃܭΛٻͯ͠ʮ8FCࢴࣳډʯʹߦ͖ண͍ͨIUUQT LBJHJPOSBJMTPSHUBMLTOBZ