Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 16 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ILT Membuat Websitemu Lebih Interaktif

Live demo: [Calm Playlist](https://calm-playlist.netlify.app/)

## Pengantar

Studi kasus ini ditujukan untuk ILT dengan materi DOM Manipulation dan Event. Ini adalah studi kasus lanjutan dari ILT belajar membangun kerangka halaman web dengan HTML dan CSS.
Expand All @@ -14,44 +16,35 @@ Branch repository ini mengandung dua direktori yang bersifat starter dan solutio

Hasil akhir dari kegiatan hands-on demo akan terlihat seperti berikut.

![Calm Headphone](calm-headphones-final.png?raw=true)
![Calm Playlist](calm-playlist-final.png?raw=true)

## Konten Calm Music

Karena akan menampilkan calm playlist, kita perlu menentukan struktur konten HTML. Berikut adalah contoh HTML yang bisa instruktur jadikan acuan.

```html
<article class="music-list__item" data-musicid="{{AUDIO ID}}">
<div class="card">
<div class="card-image">
<img id="musicImage" src="{{ART AUDIO IMAGE LINK}}" alt="{{AUDIO TITLE}}" />
</div>
<div class="card-body">
<h3 id="musicTitle" class="card-body__title">{{AUDIO TITLE}}</h3>
<div id="musicSignature" class="card-body__copyright">{{AUDIO SIGNATURE}}</div>
<audio
id="musicAudio"
class="music-list__item__audio"
controls
src="{{AUDIO LINK}}"
type="audio/mp3"
preload="none"
></audio>
</div>
<article class="music-item" data-musicid="{{AUDIO ID}}">
<img class="music-item__art" src="{{ART AUDIO IMAGE LINK}}" alt="" />
<div class="music-item__body">
<h3 class="music-item__title">{{AUDIO TITLE}}</h3>
<p class="music-item__author">{{AUDIO AUTHOR}}</p>
<audio class="music-item__audio" controls src="{{AUDIO LINK}}" preload="none"></audio>
<p class="music-item__signature">{{AUDIO SIGNATURE}}</p>
</div>
</article>
```

Karena konten di atas disalin berulang untuk setiap lagu, penandanya memakai `class`, bukan `id`.

Kode styling telah kami sediakan untuk memudahkan instruktur melakukan hands-on demo. Ini dengan catatan bahwa instruktur mengikuti struktur konten di atas.

## Checklist Live Demo

Instruktur dapat memanfaatkan checklist live demo berikut agar sesi hands-on lebih terstruktur.

- [ ] Menyiapkan starter project yang telah disediakan.
- [ ] Menjalankan proyek dengan calm playlist temporary (tersedia langsung di HTML).
- [ ] Merapikan dan mempercantik tampilan dengan CSS (jika dibutuhkan)
- [ ] Menampilkan calm playlist yang telah disediakan pada `scripts/utils.js`.
- [ ] Tampilkan menggunakan `<template>`
- [ ] Tampilkan menggunakan string (`innerHTML`).
- [ ] Menampilkan calm playlist yang telah disediakan pada `scripts/utils.js` menggunakan `<template>`.
- [ ] Membuat tombol filter berdasarkan author lagu.
- [ ] Menyaring calm playlist saat tombol filter diklik (memanfaatkan event delegation).
- [ ] Mengatur agar dua atau lebih audio tidak dimainkan secara serentak (memanfaatkan event `play`).
- [ ] Menampilkan Now Playing bar mengikuti keadaan audio (event `play`, `pause`, dan `ended`).
Binary file removed calm-headphones-final.png
Binary file not shown.
Binary file added calm-playlist-final.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/assets/images/favicon/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/assets/images/favicon/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/assets/images/favicon/favicon.ico
Binary file not shown.
19 changes: 19 additions & 0 deletions solution/assets/images/favicon/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Calm Playlist",
"short_name": "Calm",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file added solution/assets/images/headphone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
159 changes: 113 additions & 46 deletions solution/assets/scripts/script.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,140 @@
import {
ALL_AUTHORS,
getAllMusics,
generateMusicItemUsingTemplate,
generateMusicItemUsingInnerHTML,
getAllAuthors,
getMusicById,
filterMusicsByAuthor,
createMusicItemElement,
createFilterButtonElement,
setupDrawer,
renderFilterState,
showNowPlaying,
hideNowPlaying,
renderNowPlayingState,
} from './utils.js';

const drawerButton = document.querySelector('#drawer-button');
const drawerNavigation = document.querySelector('#navList');
const musicListContainer = document.getElementById('musicList');
const musicList = document.getElementById('musicList');
const filterBar = document.getElementById('filterBar');
const nowPlayingToggle = document.getElementById('nowPlayingToggle');

function setupDrawer() {
drawerButton.addEventListener('click', () => {
drawerNavigation.classList.toggle('open');
});
const musics = getAllMusics();
let activeAuthor = ALL_AUTHORS;
let currentAudio = null;
let currentMusic = null;

document.body.addEventListener('click', (event) => {
if (!drawerNavigation.contains(event.target) && !drawerButton.contains(event.target)) {
drawerNavigation.classList.remove('open');
}
/**
* Satu-satunya fungsi yang menggambar ulang daftar lagu.
* Setiap kali state berubah, panggil fungsi ini.
*/
function render() {
const filteredMusics = filterMusicsByAuthor(musics, activeAuthor);

// Kosongkan isi lama, lalu masukkan kartunya satu per satu.
musicList.textContent = '';

filteredMusics.forEach((music) => {
musicList.append(createMusicItemElement(music));
});

renderFilterState(activeAuthor, filteredMusics.length, musics.length);
}

function stopOtherAudio(currentAudio) {
const listOfAudioElement = document.querySelectorAll('audio');
function renderFilterButtons() {
filterBar.append(createFilterButtonElement(ALL_AUTHORS, 'Semua'));

listOfAudioElement.forEach((audioElement) => {
// Others audio will be paused
if (currentAudio !== audioElement) {
audioElement.pause();
}
getAllAuthors().forEach((author) => {
filterBar.append(createFilterButtonElement(author, author));
});
}

function setupOnlyOneAudioIsPlaying() {
const listOfAudioElement = document.querySelectorAll('audio');

listOfAudioElement.forEach((audioElement) => {
/**
* See: HTMLMediaElement: play event
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play_event
*/
audioElement.addEventListener('play', (event) => {
const currentAudio = event.currentTarget;
stopOtherAudio(currentAudio);
});
// Event delegation: satu listener di container, bukan di setiap tombol.
function setupFilter() {
filterBar.addEventListener('click', (event) => {
const button = event.target.closest('.filter-button');
if (!button) return;

activeAuthor = button.dataset.author;

stopCurrentAudio();
render();
});
}

function populateWithTemplate(musics) {
const elements = musics.map(generateMusicItemUsingTemplate);
musicListContainer.append(...elements);
function stopCurrentAudio() {
if (!currentAudio) return;

// Elemen yang dihapus dari DOM tidak otomatis berhenti berbunyi.
currentAudio.pause();
currentAudio = null;
currentMusic = null;
hideNowPlaying();
}

function populateWithInnerHTML(musics) {
const elements = musics.map(generateMusicItemUsingInnerHTML);
musicListContainer.innerHTML = elements.join('');
/**
* Event `play` dan `pause` tidak melakukan bubbling, jadi listener di
* container hanya bisa menangkapnya pada capture phase — itulah arti
* argumen `true` di bawah.
*
* See: HTMLMediaElement: play event
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play_event
*/
function setupAudioPlayer() {
musicList.addEventListener(
'play',
(event) => {
currentAudio = event.target;

musicList.querySelectorAll('audio').forEach((audio) => {
if (audio !== currentAudio) audio.pause();
});

// Arah DOM -> data: dari elemen audio, naik ke kartunya, lalu cari datanya.
const article = currentAudio.closest('[data-musicid]');
currentMusic = getMusicById(article.dataset.musicid);

showNowPlaying(currentMusic);
renderNowPlayingState(true, currentMusic.title);
},
true,
);

musicList.addEventListener(
'pause',
(event) => {
if (event.target === currentAudio) renderNowPlayingState(false, currentMusic.title);
},
true,
);

musicList.addEventListener('ended', stopCurrentAudio, true);

/**
* Tombol hanya memberi perintah play()/pause(). Tampilan diperbarui oleh
* listener di atas, sehingga tidak mungkin berbohong tentang keadaan audio.
*/
nowPlayingToggle.addEventListener('click', () => {
if (!currentAudio) return;

if (currentAudio.paused) {
currentAudio.play();
} else {
currentAudio.pause();
}
});
}

/**
* Semua listener dipasang pada container yang sudah ada di HTML,
* bukan pada kartu hasil render. Karena itu urutan di bawah tidak kritis.
*/
function init() {
setupDrawer();

const musicsSample = getAllMusics();

// Render using <template>
populateWithTemplate(musicsSample);

// Render using innerHTML. Uncomment next code if you prefer
// populateWithInnerHTML(musicsSample);
renderFilterButtons();
render();

setupOnlyOneAudioIsPlaying();
setupFilter();
setupAudioPlayer();
}

init();
Loading