533 lines
22 KiB
PHP
533 lines
22 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../bbs/auth.php';
|
||
edu_require_login();
|
||
|
||
require_once __DIR__ . '/../bbs/index.php';
|
||
?>
|
||
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||
<link rel="stylesheet" type="text/css" href="/edu/css/main.css" />
|
||
</head>
|
||
<body>
|
||
<div class="wrap main">
|
||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||
<div class="container">
|
||
<div class="bg-circle">
|
||
<ul>
|
||
<li><div></div></li>
|
||
<li></li>
|
||
<li></li>
|
||
<li></li>
|
||
</ul>
|
||
</div>
|
||
<div class="learning-area">
|
||
<svg id="gauge" viewBox="0 0 794 460" preserveAspectRatio="xMidYMid meet"></svg>
|
||
</div>
|
||
<div class="main-contents">
|
||
<div class="text-box">
|
||
<span><em><?= htmlspecialchars($userName) ?></em> <?= htmlspecialchars($userRank) ?>님</span>
|
||
|
||
<div class="keyword-area">
|
||
<button id="keywordSettingsBtn">나의 키워드 <i class="ico-setting"></i></button>
|
||
|
||
<!-- [DEBUG] myKeywords:<?= count($myKeywords) ?>, adminKeywords:<?= count($adminKeywords) ?>, allKeywords:<?= count($allKeywords) ?>, error:<?= htmlspecialchars($mainDataError ?? '') ?> -->
|
||
<div class="keyword-list" id="myKeyword" style="display:flex;flex-wrap:wrap;gap:4px;">
|
||
<?php foreach ($myKeywords as $i => $kw): ?>
|
||
<label class="kw-allow" for="chk_my_<?= $i ?>" style="display:inline-flex;align-items:center;gap:4px;">
|
||
<input type="checkbox" id="chk_my_<?= $i ?>" checked />
|
||
#<?= htmlspecialchars($kw['keyword_name']) ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="keyword-list" style="display:flex;flex-wrap:wrap;gap:4px;">
|
||
<?php foreach ($adminKeywords as $i => $kw): ?>
|
||
<label class="kw-deny" for="chk_admin_<?= $i ?>" style="display:inline-flex;align-items:center;gap:4px;">
|
||
<input type="checkbox" id="chk_admin_<?= $i ?>" checked />
|
||
#<?= htmlspecialchars($kw['keyword_name']) ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<p><em>취향저격 영상 추천</em>드려요.</p>
|
||
</div>
|
||
|
||
<div class="video-wrap">
|
||
<div class="video-cards-container" id="videoCardsContainer"></div>
|
||
<!-- <button class="btn-prev" id="prevBtn">‹</button>
|
||
<button class="btn-next" id="nextBtn">›</button> -->
|
||
<!-- <div class="pagination" id="pagination"><span class="current">1</span> / 1</div> -->
|
||
</div>
|
||
|
||
<?php include(__DIR__ . "/_modal/keyword.php") ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/edu/js/main/VideoCardRenderer.js" defer></script>
|
||
<script src="/edu/js/main/VideoSlider.js" defer></script>
|
||
<script src="/edu/js/main/Videomodalmanager.js" defer></script>
|
||
<script src="/edu/js/main/Gaugechart.js" defer></script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
// DEBUG: 초기 상태 확인
|
||
console.log('[INDEX DEBUG]', {
|
||
videosJson: <?= $videosJson ?>,
|
||
myKwJson: <?= $myKwJson ?>,
|
||
totalMin: <?= $totalMin ?>,
|
||
avgWatchMin: <?= $avgWatchMin ?>,
|
||
userName: <?= json_encode($userName) ?>,
|
||
userRank: <?= json_encode($userRank) ?>,
|
||
allKeywordsCount: <?= count($allKeywords) ?>,
|
||
myKeywordsCount: <?= count($myKeywords) ?>,
|
||
});
|
||
|
||
// 상세 진단 정보 조회
|
||
fetch('/edu/bbs/api/diagnosis.php')
|
||
.then(r => r.json())
|
||
.then(d => {
|
||
console.log('[DIAGNOSIS API]', d);
|
||
if (d.saved_keywords_count === 0) {
|
||
console.warn('⚠️ 저장된 키워드가 없습니다. 모달에서 키워드를 선택하고 저장해주세요.');
|
||
}
|
||
})
|
||
.catch(e => console.error('[DIAGNOSIS] fetch failed', e));
|
||
|
||
const videos = <?= $videosJson ?>;
|
||
let currentVideos = Array.isArray(videos) ? videos : [];
|
||
|
||
let allowKeywords = [];
|
||
document.querySelectorAll('#myKeyword .kw-allow input[type="checkbox"]').forEach((cb) => {
|
||
const label = cb.closest('label');
|
||
if (!label) return;
|
||
const kw = label.textContent.trim().replace(/^#/, '').trim();
|
||
if (kw) allowKeywords.push(kw);
|
||
});
|
||
if (allowKeywords.length === 0) {
|
||
allowKeywords = <?= $myKwJson ?>;
|
||
}
|
||
console.log('[KEYWORDS INIT] allowKeywords (from HTML or myKwJson):', allowKeywords);
|
||
|
||
const ACTIVE_KEY = 'edu_kw_active';
|
||
const allAdminKwList = Array.from(document.querySelectorAll('.kw-deny')).map((lbl) => lbl.textContent.trim().replace(/^#/, '').trim()).filter(Boolean);
|
||
console.log('[KEYWORDS INIT] allAdminKwList (from .kw-deny):', allAdminKwList);
|
||
|
||
let activeMyKeywords = [...allowKeywords];
|
||
let activeAdminKeywords = [...allAdminKwList];
|
||
console.log('[KEYWORDS INIT] activeMyKeywords:', activeMyKeywords, ', activeAdminKeywords:', activeAdminKeywords);
|
||
// [DEBUG] 관리자 추천 키워드 HTML 렌더링 확인
|
||
console.log('[ADMIN KEYWORDS FINAL]', {
|
||
count: document.querySelectorAll('.kw-deny').length,
|
||
keywords: Array.from(document.querySelectorAll('.kw-deny')).map(lbl => lbl.textContent.trim()),
|
||
});
|
||
const _stored = JSON.parse(localStorage.getItem(ACTIVE_KEY) || 'null');
|
||
if (_stored) {
|
||
if (Array.isArray(_stored.myActive)) {
|
||
const filtered = allowKeywords.filter((k) => _stored.myActive.includes(k));
|
||
activeMyKeywords = filtered;
|
||
}
|
||
if (Array.isArray(_stored.adminActive)) {
|
||
const filtered = allAdminKwList.filter((k) => _stored.adminActive.includes(k));
|
||
if (filtered.length > 0) activeAdminKeywords = filtered;
|
||
}
|
||
}
|
||
|
||
const keywordModal = document.getElementById('keywordModal');
|
||
const keywordSettingsBtn = document.getElementById('keywordSettingsBtn');
|
||
const keywordModalCloseBtn = keywordModal?.querySelector('.btn-close');
|
||
const keywordTagList = keywordModal?.querySelector('.keyword-tag');
|
||
|
||
function getKeywordFromLabelText(text) {
|
||
return String(text || '').trim().replace(/^#/, '').trim();
|
||
}
|
||
|
||
function getModalKeywordLabels() {
|
||
if (!keywordTagList) return [];
|
||
return Array.from(keywordTagList.querySelectorAll('label'))
|
||
.map((label) => getKeywordFromLabelText(label.textContent))
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function ensureModalKeywords() {
|
||
if (!keywordTagList) return;
|
||
|
||
const existing = getModalKeywordLabels();
|
||
if (existing.length > 0) return;
|
||
|
||
const fallbackKeywords = Array.from(new Set([
|
||
...allowKeywords,
|
||
...allAdminKwList,
|
||
...activeMyKeywords,
|
||
])).filter(Boolean);
|
||
|
||
keywordTagList.innerHTML = fallbackKeywords.map((kw, idx) => {
|
||
const safeId = `kwtag_fallback_${idx + 1}`;
|
||
return `\n<li class="kw-box">\n <label for="${safeId}">\n <input type="checkbox" id="${safeId}" />\n #${kw}\n </label>\n</li>`;
|
||
}).join('');
|
||
}
|
||
|
||
function syncModalSelection() {
|
||
if (!keywordTagList) return;
|
||
const selected = new Set(activeMyKeywords);
|
||
keywordTagList.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
||
const label = cb.closest('label');
|
||
const kw = getKeywordFromLabelText(label?.textContent || '');
|
||
cb.checked = selected.has(kw);
|
||
});
|
||
}
|
||
|
||
function renderMyKeywords(selectedKeywords) {
|
||
const myKeywordEl = document.getElementById('myKeyword');
|
||
if (!myKeywordEl) return;
|
||
|
||
myKeywordEl.innerHTML = selectedKeywords.map((kw, idx) => `\n<label class="kw-allow" for="chk_my_${idx}">\n <input type="checkbox" id="chk_my_${idx}" checked />\n #${kw}\n</label>`).join('');
|
||
}
|
||
|
||
function openKeywordModal() {
|
||
if (!keywordModal) return;
|
||
ensureModalKeywords();
|
||
syncModalSelection();
|
||
keywordModal.classList.add('is-open');
|
||
keywordModal.style.display = 'block';
|
||
keywordModal.setAttribute('aria-hidden', 'false');
|
||
document.body.classList.add('modal-open');
|
||
// 접근성: 모달 열릴 때 닫기 버튼으로 포커스 이동
|
||
keywordModalCloseBtn?.focus();
|
||
}
|
||
|
||
function closeKeywordModal(save = false) {
|
||
if (!keywordModal) return;
|
||
|
||
if (save && keywordTagList) {
|
||
const selectedMy = Array.from(keywordTagList.querySelectorAll('input[type="checkbox"]'))
|
||
.filter((cb) => cb.checked)
|
||
.map((cb) => getKeywordFromLabelText(cb.closest('label')?.textContent || ''))
|
||
.filter(Boolean)
|
||
.slice(0, 3); // 최대 3개
|
||
|
||
// 0개 포함 항상 저장 (사용자가 모두 해제한 경우도 반영)
|
||
activeMyKeywords = Array.from(new Set(selectedMy));
|
||
renderMyKeywords(activeMyKeywords);
|
||
saveActiveState();
|
||
// DB 저장 (백그라운드)
|
||
fetch('/edu/bbs/api/user_keywords.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ keywords: activeMyKeywords }),
|
||
}).then(async (res) => {
|
||
const d = await res.json().catch(() => ({}));
|
||
if (!d.success) console.warn('[user-keywords save] 실패', d);
|
||
}).catch((e) => console.warn('[user-keywords save]', e));
|
||
fetchAndRefreshVideos();
|
||
}
|
||
|
||
// 접근성: aria-hidden 설정 전에 반드시 포커스를 모달 밖으로 이동
|
||
keywordSettingsBtn?.focus();
|
||
keywordModal.classList.remove('is-open');
|
||
keywordModal.style.display = 'none';
|
||
keywordModal.setAttribute('aria-hidden', 'true');
|
||
document.body.classList.remove('modal-open');
|
||
}
|
||
|
||
keywordSettingsBtn?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
openKeywordModal();
|
||
});
|
||
|
||
keywordModalCloseBtn?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
closeKeywordModal(true);
|
||
});
|
||
|
||
keywordModal?.addEventListener('click', (e) => {
|
||
if (e.target === keywordModal) {
|
||
closeKeywordModal(false);
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && keywordModal?.classList.contains('is-open')) {
|
||
closeKeywordModal(false);
|
||
}
|
||
});
|
||
|
||
// 키워드 모달 체크박스 3개 제한
|
||
const MAX_KEYWORDS = 3;
|
||
keywordTagList?.addEventListener('change', (e) => {
|
||
const cb = e.target.closest('input[type="checkbox"]');
|
||
if (!cb || !cb.checked) return;
|
||
const checked = keywordTagList.querySelectorAll('input[type="checkbox"]:checked');
|
||
if (checked.length > MAX_KEYWORDS) {
|
||
cb.checked = false;
|
||
alert('키워드는 최대 ' + MAX_KEYWORDS + '개까지 선택 가능합니다.');
|
||
}
|
||
});
|
||
|
||
let renderer = new VideoCardRenderer({ animationDelay: 50 });
|
||
let slider = new VideoSlider({
|
||
videos: currentVideos,
|
||
videosPerPage: 6,
|
||
onPageChange: (pageVideos) => renderer.renderCards(pageVideos),
|
||
});
|
||
let modalManager = new VideoModalManager({ videos: currentVideos });
|
||
|
||
slider.init();
|
||
renderer.renderCards(slider.getCurrentPageVideos());
|
||
modalManager.init();
|
||
|
||
function extractYouTubeId(url) {
|
||
const raw = String(url || '').trim();
|
||
const matched = raw.match(/(?:v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{11})/);
|
||
if (matched && matched[1]) return matched[1];
|
||
if (/^[A-Za-z0-9_-]{11}$/.test(raw)) return raw;
|
||
return '';
|
||
}
|
||
|
||
function bindCardOpenFallback() {
|
||
const container = document.getElementById('videoCardsContainer');
|
||
if (!container || container.dataset.modalFallbackBound === '1') return;
|
||
|
||
container.dataset.modalFallbackBound = '1';
|
||
container.addEventListener('click', (e) => {
|
||
if (e.defaultPrevented) return;
|
||
|
||
const card = e.target.closest('.card[data-video-id]');
|
||
if (!card || !container.contains(card)) return;
|
||
|
||
e.preventDefault();
|
||
const videoId = card.getAttribute('data-video-id');
|
||
if (!videoId) return;
|
||
|
||
try {
|
||
if (modalManager && typeof modalManager.openVideo === 'function') {
|
||
modalManager.openVideo(videoId);
|
||
return;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[video-open fallback] modal open failed', err);
|
||
}
|
||
|
||
const selected = (Array.isArray(currentVideos) ? currentVideos : []).find((item) => String(item.id) === String(videoId));
|
||
const ytId = extractYouTubeId(selected && selected.url ? selected.url : '');
|
||
if (ytId) {
|
||
window.open(`https://www.youtube.com/watch?v=${ytId}`, '_blank', 'noopener,noreferrer');
|
||
}
|
||
});
|
||
}
|
||
|
||
async function saveWishlist(videoId, isActive) {
|
||
const params = new URLSearchParams({
|
||
content_id: String(videoId || ''),
|
||
is_active: isActive ? '1' : '0',
|
||
});
|
||
|
||
const res = await fetch('/edu/bbs/api/save_wishlist.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
body: params.toString(),
|
||
});
|
||
|
||
return res.json();
|
||
}
|
||
|
||
function setBookmarkState(videoId, isActive) {
|
||
currentVideos = (Array.isArray(currentVideos) ? currentVideos : []).map((v) => {
|
||
if (String(v.id) === String(videoId)) {
|
||
return { ...v, bookmark: !!isActive };
|
||
}
|
||
return v;
|
||
});
|
||
|
||
document.querySelectorAll(`#videoCardsContainer .card[data-video-id="${String(videoId)}"]`).forEach((card) => {
|
||
const cb = card.querySelector('.bookmark input[type="checkbox"]');
|
||
if (cb) cb.checked = !!isActive;
|
||
});
|
||
}
|
||
|
||
function bindWishlistEvents() {
|
||
const container = document.getElementById('videoCardsContainer');
|
||
if (!container || container.dataset.wishlistBound === '1') return;
|
||
container.dataset.wishlistBound = '1';
|
||
|
||
container.addEventListener('click', (e) => {
|
||
// 하트 클릭 시 카드 오픈만 막고, 체크 토글은 허용해야 change 이벤트가 발생함
|
||
if (e.target.closest('.bookmark')) {
|
||
e.stopPropagation();
|
||
}
|
||
});
|
||
|
||
container.addEventListener('change', async (e) => {
|
||
const checkbox = e.target.closest('.bookmark input[type="checkbox"]');
|
||
if (!checkbox) return;
|
||
|
||
const card = checkbox.closest('.card[data-video-id]');
|
||
const videoId = card?.getAttribute('data-video-id');
|
||
if (!videoId) return;
|
||
|
||
const nextState = !!checkbox.checked;
|
||
checkbox.disabled = true;
|
||
|
||
try {
|
||
const result = await saveWishlist(videoId, nextState);
|
||
if (!result || !result.success) {
|
||
checkbox.checked = !nextState;
|
||
console.warn('[wishlist] save failed', {
|
||
videoId,
|
||
nextState,
|
||
result,
|
||
});
|
||
return;
|
||
}
|
||
setBookmarkState(videoId, nextState);
|
||
} catch (err) {
|
||
checkbox.checked = !nextState;
|
||
console.warn('[wishlist]', err?.message || err);
|
||
} finally {
|
||
checkbox.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
bindCardOpenFallback();
|
||
bindWishlistEvents();
|
||
|
||
let gaugeMaxValue = <?= (int)$avgWatchMin ?>;
|
||
let gaugeTotalMin = <?= (int)$totalMin ?>;//추가
|
||
console.warn('[gaugeMaxValue]', gaugeMaxValue);
|
||
console.warn('[gaugeTotalMin]', gaugeTotalMin);
|
||
|
||
// const initialGaugeMinutes = Math.min(gaugeMaxValue, <?= (int)$totalMin ?>);//기존
|
||
const initialGaugeMinutes = Math.max(gaugeMaxValue, <?= (int)$totalMin ?>);//변경
|
||
let effectiveMaxValue = Math.max(gaugeMaxValue, gaugeTotalMin);//추가
|
||
|
||
console.warn('[initialGaugeMinutes]', initialGaugeMinutes);//추가
|
||
|
||
//const gauge = new GaugeChart({ size: 832, strokeWidth: 31, maxValue: gaugeMaxValue, padding: 20, outerTextOffset: 6, innerTextOffset: 35, dotRadius: 7 });//기존
|
||
const gauge = new GaugeChart({ size: 832, strokeWidth: 31, maxValue: effectiveMaxValue, padding: 20, outerTextOffset: 6, innerTextOffset: 35, dotRadius: 7 });//변경
|
||
|
||
gauge.init();
|
||
|
||
//gauge.update(initialGaugeMinutes);//기존
|
||
gauge.update(gaugeTotalMin);//변경
|
||
|
||
|
||
function saveActiveState() {
|
||
localStorage.setItem(ACTIVE_KEY, JSON.stringify({
|
||
myActive: activeMyKeywords,
|
||
adminActive: activeAdminKeywords,
|
||
}));
|
||
}
|
||
|
||
async function fetchAndRefreshVideos() {
|
||
try {
|
||
console.log('[fetchAndRefreshVideos] activeMyKeywords=' + JSON.stringify(activeMyKeywords) + ', activeAdminKeywords=' + JSON.stringify(activeAdminKeywords));
|
||
const res = await fetch('/edu/bbs/api/videos_by_keywords.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
my_keywords: activeMyKeywords,
|
||
admin_keywords: activeAdminKeywords,
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
console.log('[videos_by_keywords response]', data);
|
||
if (!data.success) {
|
||
console.error('[videos_by_keywords] API 실패', data.error);
|
||
return;
|
||
}
|
||
|
||
currentVideos = Array.isArray(data.videos) ? data.videos : [];
|
||
|
||
const apiTotalSec = Number.parseInt(data.total_watch_tm ?? data.total_all_tm, 10);
|
||
const apiTotalMin = Number.parseInt(data.total_min, 10);
|
||
// API 응답에서 최신 평균값 반영
|
||
const apiAvgWatchMin = Number.parseInt(data.avg_watch_min, 10);
|
||
if (Number.isFinite(apiAvgWatchMin) && apiAvgWatchMin > 0) {
|
||
gaugeMaxValue = apiAvgWatchMin;
|
||
if (gauge && gauge.config) {
|
||
gauge.config.maxValue = gaugeMaxValue;
|
||
}
|
||
}
|
||
|
||
|
||
let gaugeMinutes = initialGaugeMinutes;
|
||
if (Number.isFinite(apiTotalSec)) {
|
||
gaugeMinutes = Math.floor(Math.max(0, apiTotalSec) / 60);
|
||
} else if (Number.isFinite(apiTotalMin)) {
|
||
gaugeMinutes = Math.max(0, apiTotalMin);
|
||
}
|
||
/*
|
||
gaugeMinutes = Math.min(gaugeMaxValue, gaugeMinutes);
|
||
gauge.update(gaugeMinutes);
|
||
*/
|
||
//----------------------------
|
||
// [수정 후] 26.03.30
|
||
// 1. 만약 내 학습시간(gaugeMinutes)이 평균(gaugeMaxValue)보다 크다면, 차트의 최대치를 내 시간에 맞춤
|
||
if (gaugeMinutes > gaugeMaxValue) {
|
||
gauge.config.maxValue = gaugeMinutes;
|
||
// 차트 라이브러리에 따라 maxValue를 변경 후 다시 그리거나 init해야 할 수 있습니다.
|
||
}
|
||
gauge.update(gaugeMinutes);
|
||
//----------------------------
|
||
|
||
|
||
|
||
slider = new VideoSlider({
|
||
videos: currentVideos,
|
||
videosPerPage: 6,
|
||
onPageChange: (pv) => renderer.renderCards(pv),
|
||
});
|
||
slider.init();
|
||
renderer.renderCards(slider.getCurrentPageVideos());
|
||
|
||
if (modalManager) {
|
||
modalManager.config.videos = currentVideos;
|
||
}
|
||
bindCardOpenFallback();
|
||
bindWishlistEvents();
|
||
} catch (e) {
|
||
console.warn('[키워드 필터]', e.message);
|
||
}
|
||
}
|
||
|
||
// 페이지 진입 시에도 watch_tm 기반 최신 누적값으로 게이지 갱신
|
||
fetchAndRefreshVideos();
|
||
|
||
document.getElementById('myKeyword')?.addEventListener('change', (e) => {
|
||
if (e.target.type !== 'checkbox') return;
|
||
const lbl = e.target.closest('label.kw-allow');
|
||
if (!lbl) return;
|
||
const kw = lbl.textContent.trim().replace(/^#/, '').trim();
|
||
if (e.target.checked) {
|
||
if (!activeMyKeywords.includes(kw)) activeMyKeywords.push(kw);
|
||
} else {
|
||
activeMyKeywords = activeMyKeywords.filter((k) => k !== kw);
|
||
}
|
||
saveActiveState();
|
||
fetchAndRefreshVideos();
|
||
});
|
||
|
||
document.querySelectorAll('.kw-deny input[type="checkbox"]').forEach((cb) => {
|
||
cb.addEventListener('change', function () {
|
||
const adminCbs = document.querySelectorAll('.kw-deny input[type="checkbox"]');
|
||
const checkedCount = Array.from(adminCbs).filter((c) => c.checked).length;
|
||
if (checkedCount === 0) {
|
||
this.checked = true;
|
||
return;
|
||
}
|
||
activeAdminKeywords = Array.from(adminCbs)
|
||
.filter((c) => c.checked)
|
||
.map((c) => c.closest('label')?.textContent.trim().replace(/^#/, '').trim())
|
||
.filter(Boolean);
|
||
saveActiveState();
|
||
fetchAndRefreshVideos();
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|