Implement user watch history and personalized video recommendations; enhance upload functionality with tags and custom thumbnails; add admin panel for user and video management.

This commit is contained in:
2025-09-09 21:01:12 -04:00
parent 243a2a528d
commit 7490615433
9 changed files with 468 additions and 69 deletions
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel - MiniTube</title>
<link rel="stylesheet" href="style.css">
<script src="admin.js" defer></script>
</head>
<body>
<h1>Admin Panel</h1>
<section>
<h2>Delete User Account</h2>
<form id="deleteUserForm">
<input type="text" id="deleteUsername" placeholder="Username" required>
<button type="submit">Delete User</button>
</form>
<p id="userDeleteMsg"></p>
</section>
<section>
<h2>Delete Video</h2>
<form id="deleteVideoForm">
<input type="text" id="deleteVideoId" placeholder="Video ID" required>
<button type="submit">Delete Video</button>
</form>
<p id="videoDeleteMsg"></p>
</section>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
// Admin Panel JS
document.getElementById('deleteUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('deleteUsername').value.trim();
if (!username) return;
const res = await fetch('/api/admin/delete-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const data = await res.json();
document.getElementById('userDeleteMsg').textContent = data.success ? 'User deleted.' : (data.error || 'Error.');
});
document.getElementById('deleteVideoForm').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('deleteVideoId').value.trim();
if (!id) return;
const res = await fetch('/api/admin/delete-video', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const data = await res.json();
document.getElementById('videoDeleteMsg').textContent = data.success ? 'Video deleted.' : (data.error || 'Error.');
});
+117
View File
@@ -1,3 +1,102 @@
// -------------------- Recommended Videos (Personalized) --------------------
async function loadRecommendedIndex() {
const list = document.getElementById('recommendedVideos');
if (!list) return;
let videos = [];
let res;
try {
res = await fetch('/api/recommended');
if (res.ok) {
videos = await res.json();
} else {
res = await fetch('/api/videos');
videos = await res.json();
}
} catch {
res = await fetch('/api/videos');
videos = await res.json();
}
// Show top 6
const top = videos.slice(0, 6);
list.innerHTML = '';
for (const v of top) {
const li = document.createElement('li');
li.style.display = 'flex';
li.style.alignItems = 'center';
li.style.marginBottom = '1em';
li.innerHTML = `
<a href="watch.html?id=${v.id}" style="display:flex;align-items:center;text-decoration:none;color:inherit;">
<img src="/thumbnails/${v.thumbnail}" width="80" height="60" style="object-fit:cover;border-radius:6px;margin-right:10px;">
<div>
<div style="font-weight:bold;">${v.title}</div>
<div style="font-size:0.9em;color:#aaa;">by ${v.uploader}</div>
<div style="font-size:0.9em;color:#aaa;">${v.likes?.length || 0} likes</div>
</div>
</a>
`;
list.appendChild(li);
}
}
// -------------------- Watch Later & History (localStorage) --------------------
function getWatchLater() {
return JSON.parse(localStorage.getItem('watchLater') || '[]');
}
function setWatchLater(arr) {
localStorage.setItem('watchLater', JSON.stringify(arr));
}
function addToWatchLater(video) {
let arr = getWatchLater();
if (!arr.find(v => v.id === video.id)) {
arr.unshift(video);
setWatchLater(arr.slice(0, 20));
}
renderWatchLater();
}
function removeFromWatchLater(id) {
let arr = getWatchLater().filter(v => v.id !== id);
setWatchLater(arr);
renderWatchLater();
}
function renderWatchLater() {
const list = document.getElementById('watchLaterList');
if (!list) return;
const arr = getWatchLater();
list.innerHTML = '';
arr.forEach(v => {
const li = document.createElement('li');
li.innerHTML = `<a href="watch.html?id=${v.id}">${v.title}</a> <button data-id="${v.id}" class="removeLater">✕</button>`;
list.appendChild(li);
});
list.querySelectorAll('.removeLater').forEach(btn => {
btn.onclick = e => removeFromWatchLater(btn.dataset.id);
});
}
function getWatchHistory() {
return JSON.parse(localStorage.getItem('watchHistory') || '[]');
}
function setWatchHistory(arr) {
localStorage.setItem('watchHistory', JSON.stringify(arr));
}
function addToWatchHistory(video) {
let arr = getWatchHistory();
arr = arr.filter(v => v.id !== video.id);
arr.unshift(video);
setWatchHistory(arr.slice(0, 20));
renderWatchHistory();
}
function renderWatchHistory() {
const list = document.getElementById('watchHistoryList');
if (!list) return;
const arr = getWatchHistory();
list.innerHTML = '';
arr.forEach(v => {
const li = document.createElement('li');
li.innerHTML = `<a href="watch.html?id=${v.id}">${v.title}</a>`;
list.appendChild(li);
});
}
// -------------------- Load Videos on Homepage --------------------
async function loadVideos() {
try {
@@ -13,9 +112,20 @@ async function loadVideos() {
<img src="/thumbnails/${v.thumbnail}" width="120">
<a href="watch.html?id=${v.id}">${v.title}</a>
<small>by <a href="user.html?username=${v.uploader}">${v.uploader}</a></small>
<button class="addLater" data-id="${v.id}">Watch Later</button>
`;
list.appendChild(li);
});
// Add event listeners for Watch Later
list.querySelectorAll('.addLater').forEach(btn => {
btn.onclick = async e => {
const id = btn.dataset.id;
const v = videos.find(v => v.id == id);
if (v) addToWatchLater(v);
};
});
renderWatchLater();
renderWatchHistory();
} catch (err) {
console.error("Error loading videos:", err);
}
@@ -93,6 +203,9 @@ document.addEventListener("DOMContentLoaded", () => {
// Homepage video list
loadVideos();
// Recommended sidebar
loadRecommendedIndex();
// Login form
const loginForm = document.getElementById("loginForm");
if (loginForm) loginForm.addEventListener("submit", loginUser);
@@ -108,4 +221,8 @@ document.addEventListener("DOMContentLoaded", () => {
// Upload form
const uploadForm = document.getElementById("uploadForm");
if (uploadForm) uploadForm.addEventListener("submit", uploadVideo);
// Render watch later/history on all pages
renderWatchLater();
renderWatchHistory();
});
+17 -7
View File
@@ -6,12 +6,22 @@
<script src="app.js" defer></script>
</head>
<body>
<h1>MiniTube</h1>
<!-- Upload button -->
<p><a href="upload.html"><button>Upload Video</button></a></p>
<P> <a href="login.html"><button>login</button></a></P>
<ul id="videoList"></ul>
<div style="display: flex; gap: 2em; align-items: flex-start;">
<main style="flex: 2;">
<h1>MiniTube</h1>
<!-- Upload button -->
<p><a href="upload.html"><button>Upload Video</button></a></p>
<p><a href="login.html"><button>login</button></a></p>
<ul id="videoList"></ul>
</main>
<aside style="flex: 1; min-width: 220px;">
<h2>Recommended</h2>
<ul id="recommendedVideos"></ul>
<h2>Watch Later</h2>
<ul id="watchLaterList"></ul>
<h2>Watch History</h2>
<ul id="watchHistoryList"></ul>
</aside>
</div>
</body>
</html>
+33
View File
@@ -14,6 +14,39 @@
<button type="submit">Upload</button>
</form>
<label>Title</label>
<input type="text" name="title" id="videoTitleInput" placeholder="Video Title" required>
<label>Tags (comma separated)</label>
<input type="text" name="tags" id="videoTagsInput" placeholder="e.g. music,funny,cat">
<label>Uploader</label>
<input type="text" id="uploaderInput" readonly>
<label>Video File (.mp4 or .webm)</label>
<input type="file" name="video" accept=".mp4,.webm" required>
<label><input type="checkbox" id="customThumbCheck"> Upload custom thumbnail</label>
<input type="file" name="thumbnail" id="customThumbInput" accept="image/*" style="display:none">
<button type="submit">Upload</button>
</form>
<p><a href="index.html">Back to Home</a></p>
<script>
// Show/hide custom thumbnail input
document.addEventListener('DOMContentLoaded', () => {
const check = document.getElementById('customThumbCheck');
const thumbInput = document.getElementById('customThumbInput');
check.addEventListener('change', () => {
thumbInput.style.display = check.checked ? '' : 'none';
thumbInput.required = check.checked;
});
// Fetch current user for uploader field
fetch('/api/me').then(r => r.json()).then(me => {
if (me && me.username) document.getElementById('uploaderInput').value = me.username;
});
});
</script>
</body>
</html>
+104 -18
View File
@@ -5,34 +5,60 @@
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1 id="videoTitle"></h1>
<video id="videoPlayer" controls width="640"></video>
<p>Uploader: <a id="uploaderLink"></a></p>
<button id="likeBtn">Like</button>
<span id="likeCount"></span>
<div style="display: flex; gap: 2em; align-items: flex-start;">
<main style="flex: 2;">
<h1 id="videoTitle"></h1>
<video id="videoPlayer" controls width="640"></video>
<p>Uploader: <a id="uploaderLink"></a></p>
<button id="subBtn"></button>
<p id="subCount"></p>
<button id="likeBtn">Like</button>
<span id="likeCount"></span>
<h2>Change Thumbnail</h2>
<form id="thumbForm" enctype="multipart/form-data">
<input type="file" name="thumbnail" accept="image/*" required>
<button type="submit">Update Thumbnail</button>
</form>
<button id="subBtn"></button>
<p id="subCount"></p>
<h2>Comments</h2>
<ul id="comments"></ul>
<form id="commentForm">
<input type="text" name="text" placeholder="Write a comment" required>
<button type="submit">Post</button>
</form>
<div id="thumbSection" style="display:none">
<h2>Change Thumbnail</h2>
<form id="thumbForm" enctype="multipart/form-data">
<input type="file" name="thumbnail" accept="image/*" required>
<button type="submit">Update Thumbnail</button>
</form>
</div>
<h2>Comments</h2>
<ul id="comments"></ul>
<form id="commentForm">
<input type="text" name="text" placeholder="Write a comment" required>
<button type="submit">Post</button>
</form>
</main>
<aside style="flex: 1; min-width: 220px;">
<h2>Recommended</h2>
<ul id="recommendedVideos"></ul>
<h2>Watch Later</h2>
<ul id="watchLaterList"></ul>
<h2>Watch History</h2>
<ul id="watchHistoryList"></ul>
</aside>
</div>
<script>
const params = new URLSearchParams(window.location.search);
const videoId = params.get("id");
let uploader = "";
async function getCurrentUser() {
try {
const res = await fetch('/api/me');
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
async function loadVideo() {
const res = await fetch(`/api/videos/${videoId}`);
const video = await res.json();
@@ -48,8 +74,68 @@
const userRes = await fetch(`/api/user/${uploader}`);
const user = await userRes.json();
document.getElementById("subCount").textContent = `Subscribers: ${user.subscribers}`;
// Only show Change Thumbnail if current user is uploader
const me = await getCurrentUser();
if (me && me.username === uploader) {
document.getElementById("thumbSection").style.display = "block";
} else {
document.getElementById("thumbSection").style.display = "none";
}
// Load recommended videos
loadRecommended(videoId);
}
async function loadRecommended(currentId) {
let videos = [];
let res;
try {
res = await fetch('/api/recommended');
if (res.ok) {
videos = await res.json();
} else {
// Not logged in, fallback to all videos
res = await fetch('/api/videos');
videos = await res.json();
}
} catch {
// fallback
res = await fetch('/api/videos');
videos = await res.json();
}
// Remove current video
videos = videos.filter(v => v.id != currentId);
// Show top 6
const top = videos.slice(0, 6);
const list = document.getElementById('recommendedVideos');
list.innerHTML = '';
for (const v of top) {
const li = document.createElement('li');
li.style.display = 'flex';
li.style.alignItems = 'center';
li.style.marginBottom = '1em';
li.innerHTML = `
<a href="watch.html?id=${v.id}" style="display:flex;align-items:center;text-decoration:none;color:inherit;">
<img src="/thumbnails/${v.thumbnail}" width="80" height="60" style="object-fit:cover;border-radius:6px;margin-right:10px;">
<div>
<div style="font-weight:bold;">${v.title}</div>
<div style="font-size:0.9em;color:#aaa;">by ${v.uploader}</div>
<div style="font-size:0.9em;color:#aaa;">${v.likes?.length || 0} likes</div>
</div>
</a>
`;
list.appendChild(li);
}
}
// Add /api/me endpoint to server.js if not present:
// app.get("/api/me", (req, res) => {
// if (!req.session.userId) return res.status(401).json({});
// const user = users.find(u => u.id === req.session.userId);
// if (!user) return res.status(401).json({});
// res.json({ username: user.username });
// });
function renderComments(comments) {
const list = document.getElementById("comments");
list.innerHTML = "";