From 048b1b9d9dad141a6d7232e27d3f91a3d36d9190 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hi=E1=BA=BFu=20Star?=
<66201416+huzely@users.noreply.github.com>
Date: Thu, 19 Mar 2026 10:39:01 +0700
Subject: [PATCH 1/2] Build fullstack video website
---
.gitignore | 7 +
README.md | 72 +++++++---
backend/.env.example | 3 +
backend/config/db.js | 11 ++
backend/controllers/adController.js | 25 ++++
backend/controllers/announcementController.js | 23 +++
backend/controllers/commentController.js | 11 ++
backend/controllers/metaController.js | 46 ++++++
backend/controllers/settingsController.js | 18 +++
backend/controllers/statsController.js | 49 +++++++
backend/controllers/videoController.js | 118 +++++++++++++++
backend/models/Ad.js | 13 ++
backend/models/Announcement.js | 12 ++
backend/models/Category.js | 11 ++
backend/models/Comment.js | 11 ++
backend/models/Setting.js | 13 ++
backend/models/Tag.js | 10 ++
backend/models/Video.js | 17 +++
backend/models/ViewLog.js | 11 ++
backend/package.json | 20 +++
backend/routes/adRoutes.js | 9 ++
backend/routes/announcementRoutes.js | 9 ++
backend/routes/commentRoutes.js | 6 +
backend/routes/metaRoutes.js | 25 ++++
backend/routes/searchRoutes.js | 8 ++
backend/routes/settingsRoutes.js | 7 +
backend/routes/statsRoutes.js | 8 ++
backend/routes/videoRoutes.js | 12 ++
backend/seed/seedData.js | 56 ++++++++
backend/server.js | 46 ++++++
frontend/index.html | 12 ++
frontend/package.json | 22 +++
frontend/src/App.jsx | 111 ++++++++++++++
frontend/src/api/client.js | 5 +
frontend/src/components/AdBanner.jsx | 9 ++
frontend/src/components/Layout.jsx | 26 ++++
frontend/src/components/PopupAd.jsx | 27 ++++
frontend/src/components/Skeletons.jsx | 17 +++
frontend/src/components/VideoCard.jsx | 16 +++
frontend/src/main.jsx | 13 ++
frontend/src/pages/AdminPage.jsx | 135 ++++++++++++++++++
frontend/src/pages/HomePage.jsx | 47 ++++++
frontend/src/pages/UploadPage.jsx | 54 +++++++
frontend/src/pages/VideoDetailPage.jsx | 72 ++++++++++
frontend/src/styles/global.css | 63 ++++++++
frontend/vite.config.js | 9 ++
46 files changed, 1304 insertions(+), 21 deletions(-)
create mode 100644 .gitignore
create mode 100644 backend/.env.example
create mode 100644 backend/config/db.js
create mode 100644 backend/controllers/adController.js
create mode 100644 backend/controllers/announcementController.js
create mode 100644 backend/controllers/commentController.js
create mode 100644 backend/controllers/metaController.js
create mode 100644 backend/controllers/settingsController.js
create mode 100644 backend/controllers/statsController.js
create mode 100644 backend/controllers/videoController.js
create mode 100644 backend/models/Ad.js
create mode 100644 backend/models/Announcement.js
create mode 100644 backend/models/Category.js
create mode 100644 backend/models/Comment.js
create mode 100644 backend/models/Setting.js
create mode 100644 backend/models/Tag.js
create mode 100644 backend/models/Video.js
create mode 100644 backend/models/ViewLog.js
create mode 100644 backend/package.json
create mode 100644 backend/routes/adRoutes.js
create mode 100644 backend/routes/announcementRoutes.js
create mode 100644 backend/routes/commentRoutes.js
create mode 100644 backend/routes/metaRoutes.js
create mode 100644 backend/routes/searchRoutes.js
create mode 100644 backend/routes/settingsRoutes.js
create mode 100644 backend/routes/statsRoutes.js
create mode 100644 backend/routes/videoRoutes.js
create mode 100644 backend/seed/seedData.js
create mode 100644 backend/server.js
create mode 100644 frontend/index.html
create mode 100644 frontend/package.json
create mode 100644 frontend/src/App.jsx
create mode 100644 frontend/src/api/client.js
create mode 100644 frontend/src/components/AdBanner.jsx
create mode 100644 frontend/src/components/Layout.jsx
create mode 100644 frontend/src/components/PopupAd.jsx
create mode 100644 frontend/src/components/Skeletons.jsx
create mode 100644 frontend/src/components/VideoCard.jsx
create mode 100644 frontend/src/main.jsx
create mode 100644 frontend/src/pages/AdminPage.jsx
create mode 100644 frontend/src/pages/HomePage.jsx
create mode 100644 frontend/src/pages/UploadPage.jsx
create mode 100644 frontend/src/pages/VideoDetailPage.jsx
create mode 100644 frontend/src/styles/global.css
create mode 100644 frontend/vite.config.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..852696f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+.env
+backend/node_modules
+frontend/node_modules
+dist
+coverage
+.DS_Store
diff --git a/README.md b/README.md
index cdc16c5..8f916f1 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,58 @@
-# Decrypt
-### [+] Created By HTR-TECH (@***tahmid.rayat***)
-### [+] Disclaimer :
-***Decrypter is a tool to decrypt Encrypted Bash Scripts into a Readable Format.This Tool is created for Educational Purpose only.I am not responsible for any misuse of this tool.***
-
-
-
-### [+] Installation
-```apt update```
+# NightFlix Fullstack Video Website
+
+A complete fullstack video website built with React (Vite), Node.js/Express, and MongoDB. The project includes a public video browsing experience plus an admin dashboard for content, ads, announcements, analytics, and site settings.
+
+## Project structure
+
+- `backend/` – Express API, MongoDB models, seeding script, analytics endpoints
+- `frontend/` – React + Vite client with home page, detail page, upload page, and admin dashboard
+
+## Features
+
+### Public site
+- Home page with responsive dark-mode video grid
+- Search by title or tags
+- Category and tag filters
+- Video detail page with HTML5 player or iframe embeds
+- Related videos
+- Comment system
+- Popup ad, header/middle/footer banner ads
+- Announcement banner
+- Upload page
+
+### Admin panel
+- Overview cards for total videos and total views
+- Daily and monthly analytics charts
+- CRUD for videos, categories, tags, ads, and announcements
+- Site settings editor for branding and popup ads toggle
+
+## Run locally
+
+### 1) Backend
+```bash
+cd backend
+cp .env.example .env
+npm install
+npm run seed
+npm run dev
+```
-```apt install git python2 -y```
+Backend runs on `http://localhost:3000`.
-```git clone https://github.com/hax0rtahm1d/decrypt```
+### 2) Frontend
+```bash
+cd frontend
+npm install
+npm run dev
+```
-```cd decrypt```
+Frontend runs on `http://localhost:5173`.
-```python2 dec.py```
+## MongoDB
+Make sure MongoDB is running locally at the URI defined in `backend/.env`.
-### Or, Use Single Command
+Default local URI:
+```bash
+mongodb://127.0.0.1:27017/video_site
```
-apt update && apt install git python2 -y && git clone https://github.com/hax0rtahm1d/decrypt && cd decrypt && python2 dec.py
-```
-
-## [+] Find Me on :
-[](https://github.com/htr-tech)
-[](https://www.instagram.com/tahmid.rayat)
-[](https://m.me/tahmid.rayat.official)
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..54a20f3
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,3 @@
+PORT=3000
+MONGODB_URI=mongodb://127.0.0.1:27017/video_site
+FRONTEND_URL=http://localhost:5173
diff --git a/backend/config/db.js b/backend/config/db.js
new file mode 100644
index 0000000..3b9a7b9
--- /dev/null
+++ b/backend/config/db.js
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose';
+
+export async function connectDB(uri) {
+ try {
+ await mongoose.connect(uri);
+ console.log('MongoDB connected');
+ } catch (error) {
+ console.error('MongoDB connection error:', error.message);
+ process.exit(1);
+ }
+}
diff --git a/backend/controllers/adController.js b/backend/controllers/adController.js
new file mode 100644
index 0000000..c4c4e59
--- /dev/null
+++ b/backend/controllers/adController.js
@@ -0,0 +1,25 @@
+import { Ad } from '../models/Ad.js';
+
+export async function getAds(req, res) {
+ const filter = {};
+ if (req.query.active === 'true') filter.active = true;
+ const ads = await Ad.find(filter).sort({ createdAt: -1 });
+ res.json(ads);
+}
+
+export async function createAd(req, res) {
+ const ad = await Ad.create(req.body);
+ res.status(201).json(ad);
+}
+
+export async function updateAd(req, res) {
+ const ad = await Ad.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
+ if (!ad) return res.status(404).json({ message: 'Ad not found' });
+ res.json(ad);
+}
+
+export async function deleteAd(req, res) {
+ const ad = await Ad.findByIdAndDelete(req.params.id);
+ if (!ad) return res.status(404).json({ message: 'Ad not found' });
+ res.json({ message: 'Ad deleted' });
+}
diff --git a/backend/controllers/announcementController.js b/backend/controllers/announcementController.js
new file mode 100644
index 0000000..240e36f
--- /dev/null
+++ b/backend/controllers/announcementController.js
@@ -0,0 +1,23 @@
+import { Announcement } from '../models/Announcement.js';
+
+export async function getAnnouncement(req, res) {
+ const announcement = await Announcement.findOne({ active: true }).sort({ updatedAt: -1 });
+ res.json(announcement);
+}
+
+export async function createAnnouncement(req, res) {
+ const announcement = await Announcement.create(req.body);
+ res.status(201).json(announcement);
+}
+
+export async function updateAnnouncement(req, res) {
+ const announcement = await Announcement.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
+ if (!announcement) return res.status(404).json({ message: 'Announcement not found' });
+ res.json(announcement);
+}
+
+export async function deleteAnnouncement(req, res) {
+ const announcement = await Announcement.findByIdAndDelete(req.params.id);
+ if (!announcement) return res.status(404).json({ message: 'Announcement not found' });
+ res.json({ message: 'Announcement deleted' });
+}
diff --git a/backend/controllers/commentController.js b/backend/controllers/commentController.js
new file mode 100644
index 0000000..85d951d
--- /dev/null
+++ b/backend/controllers/commentController.js
@@ -0,0 +1,11 @@
+import { Comment } from '../models/Comment.js';
+
+export async function createComment(req, res) {
+ const { videoId, text } = req.body;
+ if (!videoId || !text?.trim()) {
+ return res.status(400).json({ message: 'videoId and text are required' });
+ }
+
+ const comment = await Comment.create({ videoId, text: text.trim() });
+ res.status(201).json(comment);
+}
diff --git a/backend/controllers/metaController.js b/backend/controllers/metaController.js
new file mode 100644
index 0000000..50a24b1
--- /dev/null
+++ b/backend/controllers/metaController.js
@@ -0,0 +1,46 @@
+import { Category } from '../models/Category.js';
+import { Tag } from '../models/Tag.js';
+
+export async function listCategories(req, res) {
+ const categories = await Category.find().sort({ name: 1 });
+ res.json(categories);
+}
+
+export async function createCategory(req, res) {
+ const category = await Category.create(req.body);
+ res.status(201).json(category);
+}
+
+export async function updateCategory(req, res) {
+ const category = await Category.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
+ if (!category) return res.status(404).json({ message: 'Category not found' });
+ res.json(category);
+}
+
+export async function deleteCategory(req, res) {
+ const category = await Category.findByIdAndDelete(req.params.id);
+ if (!category) return res.status(404).json({ message: 'Category not found' });
+ res.json({ message: 'Category deleted' });
+}
+
+export async function listTags(req, res) {
+ const tags = await Tag.find().sort({ name: 1 });
+ res.json(tags);
+}
+
+export async function createTag(req, res) {
+ const tag = await Tag.create(req.body);
+ res.status(201).json(tag);
+}
+
+export async function updateTag(req, res) {
+ const tag = await Tag.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
+ if (!tag) return res.status(404).json({ message: 'Tag not found' });
+ res.json(tag);
+}
+
+export async function deleteTag(req, res) {
+ const tag = await Tag.findByIdAndDelete(req.params.id);
+ if (!tag) return res.status(404).json({ message: 'Tag not found' });
+ res.json({ message: 'Tag deleted' });
+}
diff --git a/backend/controllers/settingsController.js b/backend/controllers/settingsController.js
new file mode 100644
index 0000000..9ee7c31
--- /dev/null
+++ b/backend/controllers/settingsController.js
@@ -0,0 +1,18 @@
+import { Setting } from '../models/Setting.js';
+
+export async function getSettings(req, res) {
+ const settings = await Setting.findOne().sort({ updatedAt: -1 });
+ res.json(settings);
+}
+
+export async function saveSettings(req, res) {
+ const existing = await Setting.findOne();
+ if (existing) {
+ Object.assign(existing, req.body);
+ await existing.save();
+ return res.json(existing);
+ }
+
+ const settings = await Setting.create(req.body);
+ res.status(201).json(settings);
+}
diff --git a/backend/controllers/statsController.js b/backend/controllers/statsController.js
new file mode 100644
index 0000000..080892d
--- /dev/null
+++ b/backend/controllers/statsController.js
@@ -0,0 +1,49 @@
+import { Video } from '../models/Video.js';
+import { ViewLog } from '../models/ViewLog.js';
+
+export async function getOverview(req, res) {
+ const [totalVideos, totalViewsAgg] = await Promise.all([
+ Video.countDocuments(),
+ Video.aggregate([{ $group: { _id: null, totalViews: { $sum: '$views' } } }])
+ ]);
+
+ res.json({
+ totalVideos,
+ totalViews: totalViewsAgg[0]?.totalViews || 0
+ });
+}
+
+export async function getDaily(req, res) {
+ const data = await ViewLog.aggregate([
+ {
+ $group: {
+ _id: {
+ year: { $year: '$createdAt' },
+ month: { $month: '$createdAt' },
+ day: { $dayOfMonth: '$createdAt' }
+ },
+ count: { $sum: 1 }
+ }
+ },
+ { $sort: { '_id.year': 1, '_id.month': 1, '_id.day': 1 } }
+ ]);
+
+ res.json(data.map((item) => ({ date: `${item._id.year}-${String(item._id.month).padStart(2, '0')}-${String(item._id.day).padStart(2, '0')}`, count: item.count })));
+}
+
+export async function getMonthly(req, res) {
+ const data = await ViewLog.aggregate([
+ {
+ $group: {
+ _id: {
+ year: { $year: '$createdAt' },
+ month: { $month: '$createdAt' }
+ },
+ count: { $sum: 1 }
+ }
+ },
+ { $sort: { '_id.year': 1, '_id.month': 1 } }
+ ]);
+
+ res.json(data.map((item) => ({ month: `${item._id.year}-${String(item._id.month).padStart(2, '0')}`, count: item.count })));
+}
diff --git a/backend/controllers/videoController.js b/backend/controllers/videoController.js
new file mode 100644
index 0000000..87560e0
--- /dev/null
+++ b/backend/controllers/videoController.js
@@ -0,0 +1,118 @@
+import mongoose from 'mongoose';
+import { Video } from '../models/Video.js';
+import { Comment } from '../models/Comment.js';
+import { ViewLog } from '../models/ViewLog.js';
+import { Tag } from '../models/Tag.js';
+import { Category } from '../models/Category.js';
+
+function normalizeTags(tags) {
+ if (Array.isArray(tags)) return tags.map((tag) => String(tag).trim()).filter(Boolean);
+ if (typeof tags === 'string') {
+ return tags.split(',').map((tag) => tag.trim()).filter(Boolean);
+ }
+ return [];
+}
+
+async function syncMetadata(category, tags) {
+ if (category) {
+ await Category.findOneAndUpdate(
+ { name: category.trim() },
+ { $setOnInsert: { name: category.trim() } },
+ { upsert: true, new: true }
+ );
+ }
+
+ if (tags.length) {
+ await Promise.all(
+ tags.map((name) =>
+ Tag.findOneAndUpdate({ name }, { $setOnInsert: { name } }, { upsert: true, new: true })
+ )
+ );
+ }
+}
+
+export async function getVideos(req, res) {
+ const { category, tag, q } = req.query;
+ const filter = {};
+
+ if (category) filter.category = category;
+ if (tag) filter.tags = tag;
+ if (q) {
+ filter.$or = [
+ { title: { $regex: q, $options: 'i' } },
+ { tags: { $elemMatch: { $regex: q, $options: 'i' } } }
+ ];
+ }
+
+ const videos = await Video.find(filter).sort({ createdAt: -1 });
+ res.json(videos);
+}
+
+export async function getVideoById(req, res) {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({ message: 'Invalid video id' });
+ }
+
+ const video = await Video.findById(id);
+ if (!video) {
+ return res.status(404).json({ message: 'Video not found' });
+ }
+
+ video.views += 1;
+ await video.save();
+
+ await ViewLog.create({
+ videoId: video._id,
+ ip: req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || 'unknown'
+ });
+
+ const comments = await Comment.find({ videoId: video._id }).sort({ createdAt: -1 });
+ const relatedVideos = await Video.find({
+ _id: { $ne: video._id },
+ $or: [{ category: video.category }, { tags: { $in: video.tags } }]
+ })
+ .limit(8)
+ .sort({ views: -1, createdAt: -1 });
+
+ return res.json({ ...video.toObject(), comments, relatedVideos });
+}
+
+export async function createVideo(req, res) {
+ const payload = req.body;
+ const tags = normalizeTags(payload.tags);
+ const video = await Video.create({ ...payload, tags });
+ await syncMetadata(video.category, tags);
+ res.status(201).json(video);
+}
+
+export async function updateVideo(req, res) {
+ const { id } = req.params;
+ const payload = req.body;
+ const tags = normalizeTags(payload.tags);
+ const video = await Video.findByIdAndUpdate(id, { ...payload, tags }, { new: true, runValidators: true });
+ if (!video) return res.status(404).json({ message: 'Video not found' });
+ await syncMetadata(video.category, tags);
+ res.json(video);
+}
+
+export async function deleteVideo(req, res) {
+ const { id } = req.params;
+ const video = await Video.findByIdAndDelete(id);
+ if (!video) return res.status(404).json({ message: 'Video not found' });
+ await Comment.deleteMany({ videoId: id });
+ await ViewLog.deleteMany({ videoId: id });
+ res.json({ message: 'Video deleted' });
+}
+
+export async function searchVideos(req, res) {
+ const q = req.query.q || '';
+ const videos = await Video.find({
+ $or: [
+ { title: { $regex: q, $options: 'i' } },
+ { tags: { $elemMatch: { $regex: q, $options: 'i' } } }
+ ]
+ }).sort({ createdAt: -1 });
+
+ res.json(videos);
+}
diff --git a/backend/models/Ad.js b/backend/models/Ad.js
new file mode 100644
index 0000000..cad855b
--- /dev/null
+++ b/backend/models/Ad.js
@@ -0,0 +1,13 @@
+import mongoose from 'mongoose';
+
+const adSchema = new mongoose.Schema(
+ {
+ image: { type: String, required: true },
+ link: { type: String, required: true },
+ position: { type: String, enum: ['popup', 'header', 'middle', 'footer'], required: true },
+ active: { type: Boolean, default: true }
+ },
+ { timestamps: true }
+);
+
+export const Ad = mongoose.model('Ad', adSchema);
diff --git a/backend/models/Announcement.js b/backend/models/Announcement.js
new file mode 100644
index 0000000..ccf9792
--- /dev/null
+++ b/backend/models/Announcement.js
@@ -0,0 +1,12 @@
+import mongoose from 'mongoose';
+
+const announcementSchema = new mongoose.Schema(
+ {
+ title: { type: String, required: true },
+ content: { type: String, required: true },
+ active: { type: Boolean, default: true }
+ },
+ { timestamps: true }
+);
+
+export const Announcement = mongoose.model('Announcement', announcementSchema);
diff --git a/backend/models/Category.js b/backend/models/Category.js
new file mode 100644
index 0000000..2b4bc60
--- /dev/null
+++ b/backend/models/Category.js
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose';
+
+const categorySchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true, unique: true, trim: true },
+ description: { type: String, default: '' }
+ },
+ { timestamps: true }
+);
+
+export const Category = mongoose.model('Category', categorySchema);
diff --git a/backend/models/Comment.js b/backend/models/Comment.js
new file mode 100644
index 0000000..44be812
--- /dev/null
+++ b/backend/models/Comment.js
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose';
+
+const commentSchema = new mongoose.Schema(
+ {
+ videoId: { type: mongoose.Schema.Types.ObjectId, ref: 'Video', required: true },
+ text: { type: String, required: true, trim: true }
+ },
+ { timestamps: true }
+);
+
+export const Comment = mongoose.model('Comment', commentSchema);
diff --git a/backend/models/Setting.js b/backend/models/Setting.js
new file mode 100644
index 0000000..9f6dc30
--- /dev/null
+++ b/backend/models/Setting.js
@@ -0,0 +1,13 @@
+import mongoose from 'mongoose';
+
+const settingSchema = new mongoose.Schema(
+ {
+ siteName: { type: String, default: 'NightFlix' },
+ logo: { type: String, default: '' },
+ primaryColor: { type: String, default: '#e50914' },
+ popupAdsEnabled: { type: Boolean, default: true }
+ },
+ { timestamps: true }
+);
+
+export const Setting = mongoose.model('Setting', settingSchema);
diff --git a/backend/models/Tag.js b/backend/models/Tag.js
new file mode 100644
index 0000000..2eb4824
--- /dev/null
+++ b/backend/models/Tag.js
@@ -0,0 +1,10 @@
+import mongoose from 'mongoose';
+
+const tagSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true, unique: true, trim: true }
+ },
+ { timestamps: true }
+);
+
+export const Tag = mongoose.model('Tag', tagSchema);
diff --git a/backend/models/Video.js b/backend/models/Video.js
new file mode 100644
index 0000000..da2074d
--- /dev/null
+++ b/backend/models/Video.js
@@ -0,0 +1,17 @@
+import mongoose from 'mongoose';
+
+const videoSchema = new mongoose.Schema(
+ {
+ title: { type: String, required: true, trim: true },
+ description: { type: String, default: '' },
+ thumbnail: { type: String, required: true },
+ videoUrl: { type: String, required: true },
+ type: { type: String, enum: ['mp4', 'embed'], required: true },
+ views: { type: Number, default: 0 },
+ tags: [{ type: String, trim: true }],
+ category: { type: String, required: true, trim: true }
+ },
+ { timestamps: true }
+);
+
+export const Video = mongoose.model('Video', videoSchema);
diff --git a/backend/models/ViewLog.js b/backend/models/ViewLog.js
new file mode 100644
index 0000000..2bb05e7
--- /dev/null
+++ b/backend/models/ViewLog.js
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose';
+
+const viewLogSchema = new mongoose.Schema(
+ {
+ videoId: { type: mongoose.Schema.Types.ObjectId, ref: 'Video', required: true },
+ ip: { type: String, default: 'unknown' }
+ },
+ { timestamps: true }
+);
+
+export const ViewLog = mongoose.model('ViewLog', viewLogSchema);
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..b437b40
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "video-site-backend",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "nodemon server.js",
+ "start": "node server.js",
+ "seed": "node seed/seedData.js"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "mongoose": "^8.6.1",
+ "morgan": "^1.10.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.4"
+ }
+}
diff --git a/backend/routes/adRoutes.js b/backend/routes/adRoutes.js
new file mode 100644
index 0000000..a6c085e
--- /dev/null
+++ b/backend/routes/adRoutes.js
@@ -0,0 +1,9 @@
+import { Router } from 'express';
+import { createAd, deleteAd, getAds, updateAd } from '../controllers/adController.js';
+
+const router = Router();
+router.get('/', getAds);
+router.post('/', createAd);
+router.put('/:id', updateAd);
+router.delete('/:id', deleteAd);
+export default router;
diff --git a/backend/routes/announcementRoutes.js b/backend/routes/announcementRoutes.js
new file mode 100644
index 0000000..aee8ca5
--- /dev/null
+++ b/backend/routes/announcementRoutes.js
@@ -0,0 +1,9 @@
+import { Router } from 'express';
+import { createAnnouncement, deleteAnnouncement, getAnnouncement, updateAnnouncement } from '../controllers/announcementController.js';
+
+const router = Router();
+router.get('/', getAnnouncement);
+router.post('/', createAnnouncement);
+router.put('/:id', updateAnnouncement);
+router.delete('/:id', deleteAnnouncement);
+export default router;
diff --git a/backend/routes/commentRoutes.js b/backend/routes/commentRoutes.js
new file mode 100644
index 0000000..4027a3f
--- /dev/null
+++ b/backend/routes/commentRoutes.js
@@ -0,0 +1,6 @@
+import { Router } from 'express';
+import { createComment } from '../controllers/commentController.js';
+
+const router = Router();
+router.post('/', createComment);
+export default router;
diff --git a/backend/routes/metaRoutes.js b/backend/routes/metaRoutes.js
new file mode 100644
index 0000000..ba57116
--- /dev/null
+++ b/backend/routes/metaRoutes.js
@@ -0,0 +1,25 @@
+import { Router } from 'express';
+import {
+ createCategory,
+ createTag,
+ deleteCategory,
+ deleteTag,
+ listCategories,
+ listTags,
+ updateCategory,
+ updateTag
+} from '../controllers/metaController.js';
+
+const categoryRouter = Router();
+categoryRouter.get('/', listCategories);
+categoryRouter.post('/', createCategory);
+categoryRouter.put('/:id', updateCategory);
+categoryRouter.delete('/:id', deleteCategory);
+
+const tagRouter = Router();
+tagRouter.get('/', listTags);
+tagRouter.post('/', createTag);
+tagRouter.put('/:id', updateTag);
+tagRouter.delete('/:id', deleteTag);
+
+export { categoryRouter, tagRouter };
diff --git a/backend/routes/searchRoutes.js b/backend/routes/searchRoutes.js
new file mode 100644
index 0000000..8983fdc
--- /dev/null
+++ b/backend/routes/searchRoutes.js
@@ -0,0 +1,8 @@
+import { Router } from 'express';
+import { searchVideos } from '../controllers/videoController.js';
+
+const router = Router();
+
+router.get('/', searchVideos);
+
+export default router;
diff --git a/backend/routes/settingsRoutes.js b/backend/routes/settingsRoutes.js
new file mode 100644
index 0000000..c325434
--- /dev/null
+++ b/backend/routes/settingsRoutes.js
@@ -0,0 +1,7 @@
+import { Router } from 'express';
+import { getSettings, saveSettings } from '../controllers/settingsController.js';
+
+const router = Router();
+router.get('/', getSettings);
+router.post('/', saveSettings);
+export default router;
diff --git a/backend/routes/statsRoutes.js b/backend/routes/statsRoutes.js
new file mode 100644
index 0000000..4484775
--- /dev/null
+++ b/backend/routes/statsRoutes.js
@@ -0,0 +1,8 @@
+import { Router } from 'express';
+import { getDaily, getMonthly, getOverview } from '../controllers/statsController.js';
+
+const router = Router();
+router.get('/overview', getOverview);
+router.get('/daily', getDaily);
+router.get('/monthly', getMonthly);
+export default router;
diff --git a/backend/routes/videoRoutes.js b/backend/routes/videoRoutes.js
new file mode 100644
index 0000000..2f002f4
--- /dev/null
+++ b/backend/routes/videoRoutes.js
@@ -0,0 +1,12 @@
+import { Router } from 'express';
+import { createVideo, deleteVideo, getVideoById, getVideos, searchVideos, updateVideo } from '../controllers/videoController.js';
+
+const router = Router();
+
+router.get('/', getVideos);
+router.get('/:id', getVideoById);
+router.post('/', createVideo);
+router.put('/:id', updateVideo);
+router.delete('/:id', deleteVideo);
+
+export default router;
diff --git a/backend/seed/seedData.js b/backend/seed/seedData.js
new file mode 100644
index 0000000..6ea6dc5
--- /dev/null
+++ b/backend/seed/seedData.js
@@ -0,0 +1,56 @@
+import 'dotenv/config';
+import { connectDB } from '../config/db.js';
+import { Video } from '../models/Video.js';
+import { Category } from '../models/Category.js';
+import { Tag } from '../models/Tag.js';
+import { Ad } from '../models/Ad.js';
+import { Announcement } from '../models/Announcement.js';
+import { Setting } from '../models/Setting.js';
+
+const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/video_site';
+
+const categories = ['Trending', 'Featured', 'Action', 'Drama', 'Music'];
+const tags = ['hot', 'hd', 'exclusive', 'trending', 'viral', 'night', 'cinema'];
+const videos = Array.from({ length: 10 }).map((_, index) => ({
+ title: `Sample Video ${index + 1}`,
+ description: `Demo description for sample video ${index + 1}.`,
+ thumbnail: `https://picsum.photos/seed/video-${index + 1}/640/360`,
+ videoUrl: index % 2 === 0 ? 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4' : 'https://www.youtube.com/embed/dQw4w9WgXcQ',
+ type: index % 2 === 0 ? 'mp4' : 'embed',
+ views: 1000 + index * 137,
+ tags: [tags[index % tags.length], tags[(index + 2) % tags.length]],
+ category: categories[index % categories.length],
+ createdAt: new Date(Date.now() - index * 86400000)
+}));
+
+const ads = [
+ { image: 'https://picsum.photos/seed/header-ad/1200/180', link: 'https://example.com/header', position: 'header', active: true },
+ { image: 'https://picsum.photos/seed/middle-ad/1200/180', link: 'https://example.com/middle', position: 'middle', active: true },
+ { image: 'https://picsum.photos/seed/footer-ad/1200/180', link: 'https://example.com/footer', position: 'footer', active: true },
+ { image: 'https://picsum.photos/seed/popup-ad/1280/720', link: 'https://example.com/popup', position: 'popup', active: true }
+];
+
+async function seed() {
+ await connectDB(MONGODB_URI);
+
+ await Promise.all([
+ Video.deleteMany({}),
+ Category.deleteMany({}),
+ Tag.deleteMany({}),
+ Ad.deleteMany({}),
+ Announcement.deleteMany({}),
+ Setting.deleteMany({})
+ ]);
+
+ await Category.insertMany(categories.map((name) => ({ name })));
+ await Tag.insertMany(tags.map((name) => ({ name })));
+ await Video.insertMany(videos);
+ await Ad.insertMany(ads);
+ await Announcement.create({ title: 'Tonight\'s Featured Drop', content: 'New videos are live now. Browse the latest uploads and trending picks.', active: true });
+ await Setting.create({ siteName: 'NightFlix', logo: 'https://picsum.photos/seed/logo/120/40', primaryColor: '#e50914', popupAdsEnabled: true });
+
+ console.log('Seed complete');
+ process.exit(0);
+}
+
+seed();
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 0000000..999c5d5
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,46 @@
+import 'dotenv/config';
+import express from 'express';
+import cors from 'cors';
+import morgan from 'morgan';
+import { connectDB } from './config/db.js';
+import videoRoutes from './routes/videoRoutes.js';
+import searchRoutes from './routes/searchRoutes.js';
+import commentRoutes from './routes/commentRoutes.js';
+import { categoryRouter, tagRouter } from './routes/metaRoutes.js';
+import statsRoutes from './routes/statsRoutes.js';
+import adRoutes from './routes/adRoutes.js';
+import announcementRoutes from './routes/announcementRoutes.js';
+import settingsRoutes from './routes/settingsRoutes.js';
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/video_site';
+
+app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:5173' }));
+app.use(express.json());
+app.use(morgan('dev'));
+
+app.get('/api/health', (req, res) => {
+ res.json({ status: 'ok' });
+});
+
+app.use('/api/videos', videoRoutes);
+app.use('/api/search', searchRoutes);
+app.use('/api/comments', commentRoutes);
+app.use('/api/categories', categoryRouter);
+app.use('/api/tags', tagRouter);
+app.use('/api/stats', statsRoutes);
+app.use('/api/ads', adRoutes);
+app.use('/api/announcement', announcementRoutes);
+app.use('/api/settings', settingsRoutes);
+
+app.use((error, req, res, next) => {
+ console.error(error);
+ res.status(500).json({ message: 'Internal server error' });
+});
+
+connectDB(MONGODB_URI).then(() => {
+ app.listen(PORT, () => {
+ console.log(`Backend running on port ${PORT}`);
+ });
+});
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..6708a38
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ NightFlix
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..650a170
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "video-site-frontend",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.7.4",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.26.1",
+ "recharts": "^2.12.7"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.1",
+ "vite": "^5.4.2"
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..a3b79d5
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,111 @@
+import { useEffect, useMemo, useState } from 'react';
+import { Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
+import { api } from './api/client';
+import { Layout } from './components/Layout';
+import { PopupAd } from './components/PopupAd';
+import { HomePage } from './pages/HomePage';
+import { VideoDetailPage } from './pages/VideoDetailPage';
+import { UploadPage } from './pages/UploadPage';
+import { AdminPage } from './pages/AdminPage';
+
+function VideoDetailRoute({ fetchVideoDetail, videoDetail, loadingVideo }) {
+ const { id } = useParams();
+ useEffect(() => {
+ fetchVideoDetail(id);
+ }, [id]);
+ return fetchVideoDetail(id)} />;
+}
+
+export default function App() {
+ const [videos, setVideos] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [tags, setTags] = useState([]);
+ const [ads, setAds] = useState([]);
+ const [announcement, setAnnouncement] = useState(null);
+ const [settings, setSettings] = useState(null);
+ const [stats, setStats] = useState(null);
+ const [daily, setDaily] = useState([]);
+ const [monthly, setMonthly] = useState([]);
+ const [loadingVideos, setLoadingVideos] = useState(true);
+ const [loadingVideo, setLoadingVideo] = useState(false);
+ const [loadingStats, setLoadingStats] = useState(true);
+ const [videoDetail, setVideoDetail] = useState(null);
+ const [search, setSearch] = useState('');
+ const location = useLocation();
+ const navigate = useNavigate();
+ const query = useMemo(() => new URLSearchParams(location.search), [location.search]);
+ const selectedCategory = query.get('category') || '';
+ const selectedTag = query.get('tag') || '';
+
+ const loadInitial = async () => {
+ setLoadingVideos(true);
+ const params = {};
+ if (selectedCategory) params.category = selectedCategory;
+ if (selectedTag) params.tag = selectedTag;
+ if (search.trim()) params.q = search.trim();
+
+ const [videosRes, categoriesRes, tagsRes, adsRes, announcementRes, settingsRes] = await Promise.all([
+ api.get('/videos', { params }),
+ api.get('/categories'),
+ api.get('/tags'),
+ api.get('/ads', { params: { active: true } }),
+ api.get('/announcement'),
+ api.get('/settings')
+ ]);
+
+ setVideos(videosRes.data);
+ setCategories(categoriesRes.data);
+ setTags(tagsRes.data);
+ setAds(adsRes.data);
+ setAnnouncement(announcementRes.data);
+ setSettings(settingsRes.data);
+ setLoadingVideos(false);
+ };
+
+ const loadStats = async () => {
+ setLoadingStats(true);
+ const [overviewRes, dailyRes, monthlyRes] = await Promise.all([
+ api.get('/stats/overview'),
+ api.get('/stats/daily'),
+ api.get('/stats/monthly')
+ ]);
+ setStats(overviewRes.data);
+ setDaily(dailyRes.data);
+ setMonthly(monthlyRes.data);
+ setLoadingStats(false);
+ };
+
+ const fetchVideoDetail = async (id) => {
+ setLoadingVideo(true);
+ const response = await api.get(`/videos/${id}`);
+ setVideoDetail(response.data);
+ setLoadingVideo(false);
+ };
+
+ useEffect(() => {
+ loadInitial();
+ }, [selectedCategory, selectedTag, search]);
+
+ useEffect(() => {
+ loadStats();
+ }, []);
+
+ const setFilter = (key, value) => {
+ const next = new URLSearchParams(location.search);
+ if (value) next.set(key, value);
+ else next.delete(key);
+ navigate({ pathname: '/', search: next.toString() });
+ };
+
+ return (
+
+ ad.position === 'popup')} enabled={Boolean(settings?.popupAdsEnabled)} />
+
+ setFilter('category', value)} selectedTag={selectedTag} onTagChange={(value) => setFilter('tag', value)} announcement={announcement} ads={ads} />} />
+ } />
+ } />
+ { loadInitial(); loadStats(); }} />} />
+
+
+ );
+}
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
new file mode 100644
index 0000000..463a0fc
--- /dev/null
+++ b/frontend/src/api/client.js
@@ -0,0 +1,5 @@
+import axios from 'axios';
+
+export const api = axios.create({
+ baseURL: 'http://localhost:3000/api'
+});
diff --git a/frontend/src/components/AdBanner.jsx b/frontend/src/components/AdBanner.jsx
new file mode 100644
index 0000000..f9dc6f4
--- /dev/null
+++ b/frontend/src/components/AdBanner.jsx
@@ -0,0 +1,9 @@
+export function AdBanner({ ad }) {
+ if (!ad?.active) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
new file mode 100644
index 0000000..88e4b99
--- /dev/null
+++ b/frontend/src/components/Layout.jsx
@@ -0,0 +1,26 @@
+import { Link, NavLink } from 'react-router-dom';
+
+export function Layout({ settings, search, onSearchChange, children }) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/PopupAd.jsx b/frontend/src/components/PopupAd.jsx
new file mode 100644
index 0000000..c37fd2b
--- /dev/null
+++ b/frontend/src/components/PopupAd.jsx
@@ -0,0 +1,27 @@
+import { useEffect, useState } from 'react';
+
+export function PopupAd({ ad, enabled }) {
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ if (!enabled || !ad?.active) return;
+ const alreadySeen = sessionStorage.getItem('popupSeen');
+ if (!alreadySeen) {
+ setOpen(true);
+ sessionStorage.setItem('popupSeen', 'true');
+ }
+ }, [ad, enabled]);
+
+ if (!open || !ad?.active || !enabled) return null;
+
+ return (
+ setOpen(false)}>
+
event.stopPropagation()}>
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/Skeletons.jsx b/frontend/src/components/Skeletons.jsx
new file mode 100644
index 0000000..141cadd
--- /dev/null
+++ b/frontend/src/components/Skeletons.jsx
@@ -0,0 +1,17 @@
+export function VideoGridSkeleton() {
+ return (
+
+ {Array.from({ length: 8 }).map((_, index) => (
+
+ ))}
+
+ );
+}
+
+export function ChartSkeleton() {
+ return ;
+}
diff --git a/frontend/src/components/VideoCard.jsx b/frontend/src/components/VideoCard.jsx
new file mode 100644
index 0000000..c3c82bf
--- /dev/null
+++ b/frontend/src/components/VideoCard.jsx
@@ -0,0 +1,16 @@
+import { Link } from 'react-router-dom';
+
+export function VideoCard({ video }) {
+ return (
+
+
+

+
+
+
{video.title}
+
{video.views.toLocaleString()} views
+
{new Date(video.createdAt).toLocaleDateString()}
+
+
+ );
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..1f82009
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+import './styles/global.css';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+);
diff --git a/frontend/src/pages/AdminPage.jsx b/frontend/src/pages/AdminPage.jsx
new file mode 100644
index 0000000..ddb2dce
--- /dev/null
+++ b/frontend/src/pages/AdminPage.jsx
@@ -0,0 +1,135 @@
+import { useEffect, useMemo, useState } from 'react';
+import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
+import { api } from '../api/client';
+import { ChartSkeleton } from '../components/Skeletons';
+
+function CrudList({ title, items, fields, onCreate, onUpdate, onDelete, initialState }) {
+ const [form, setForm] = useState(initialState);
+ const [editingId, setEditingId] = useState('');
+
+ const submit = async (event) => {
+ event.preventDefault();
+ if (editingId) {
+ await onUpdate(editingId, form);
+ } else {
+ await onCreate(form);
+ }
+ setForm(initialState);
+ setEditingId('');
+ };
+
+ return (
+
+ {title}
+
+
+ {items.map((item) => (
+
+
+
{item.name || item.title || item.siteName || item.position}
+ {item.content ?
{item.content}
: null}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
+export function AdminPage({ stats, daily, monthly, loadingStats, videos, categories, tags, ads, announcement, settings, refreshAll }) {
+ const [settingsForm, setSettingsForm] = useState(settings || {});
+ useEffect(() => {
+ setSettingsForm(settings || {});
+ }, [settings]);
+ const announcementItems = announcement ? [announcement] : [];
+ const overviewCards = useMemo(() => [
+ { label: 'Total Videos', value: stats?.totalVideos ?? 0 },
+ { label: 'Total Views', value: stats?.totalViews ?? 0 }
+ ], [stats]);
+
+ const createEntity = (path) => (payload) => api.post(path, payload).then(refreshAll);
+ const updateEntity = (path) => (id, payload) => api.put(`${path}/${id}`, payload).then(refreshAll);
+ const deleteEntity = (path) => (id) => api.delete(`${path}/${id}`).then(refreshAll);
+
+ const saveSettings = async (event) => {
+ event.preventDefault();
+ await api.post('/settings', settingsForm);
+ refreshAll();
+ };
+
+ return (
+
+
+ {overviewCards.map((card) => (
+
+ {card.label}
+ {card.value}
+
+ ))}
+
+
+
+
Daily Views
+ {loadingStats ? : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
Monthly Views
+ {loadingStats ? : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx
new file mode 100644
index 0000000..f387a1b
--- /dev/null
+++ b/frontend/src/pages/HomePage.jsx
@@ -0,0 +1,47 @@
+import { useMemo } from 'react';
+import { VideoCard } from '../components/VideoCard';
+import { VideoGridSkeleton } from '../components/Skeletons';
+import { AdBanner } from '../components/AdBanner';
+
+export function HomePage({ videos, loading, categories, selectedCategory, onCategoryChange, selectedTag, onTagChange, announcement, ads }) {
+ const tags = useMemo(() => [...new Set(videos.flatMap((video) => video.tags || []))], [videos]);
+ const headerAd = ads.find((ad) => ad.position === 'header' && ad.active);
+ const middleAd = ads.find((ad) => ad.position === 'middle' && ad.active);
+ const footerAd = ads.find((ad) => ad.position === 'footer' && ad.active);
+
+ return (
+
+
+ {announcement ? (
+
+ {announcement.title}
+ {announcement.content}
+
+ ) : null}
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+ {tags.map((tag) => (
+
+ ))}
+
+ {loading ?
:
{videos.map((video) => )}
}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/UploadPage.jsx b/frontend/src/pages/UploadPage.jsx
new file mode 100644
index 0000000..2bd8447
--- /dev/null
+++ b/frontend/src/pages/UploadPage.jsx
@@ -0,0 +1,54 @@
+import { useState } from 'react';
+import { api } from '../api/client';
+
+const initialState = {
+ title: '',
+ description: '',
+ thumbnail: '',
+ videoUrl: '',
+ type: 'mp4',
+ category: '',
+ tags: ''
+};
+
+export function UploadPage({ onUploaded }) {
+ const [form, setForm] = useState(initialState);
+ const [saving, setSaving] = useState(false);
+
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+ setSaving(true);
+ await api.post('/videos', form);
+ setForm(initialState);
+ setSaving(false);
+ onUploaded();
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/VideoDetailPage.jsx b/frontend/src/pages/VideoDetailPage.jsx
new file mode 100644
index 0000000..ade9f50
--- /dev/null
+++ b/frontend/src/pages/VideoDetailPage.jsx
@@ -0,0 +1,72 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { api } from '../api/client';
+import { VideoGridSkeleton } from '../components/Skeletons';
+
+export function VideoDetailPage({ video, loading, onRefresh }) {
+ const [comment, setComment] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+
+ if (loading) return ;
+ if (!video) return Video not found.
;
+
+ const submitComment = async (event) => {
+ event.preventDefault();
+ if (!comment.trim()) return;
+ setSubmitting(true);
+ await api.post('/comments', { videoId: video._id, text: comment });
+ setComment('');
+ await onRefresh();
+ setSubmitting(false);
+ };
+
+ return (
+
+
+ {video.type === 'mp4' ? (
+
+ ) : (
+
+ )}
+ {video.title}
+ {video.views.toLocaleString()} views • {new Date(video.createdAt).toLocaleDateString()}
+ {video.description}
+
+ {video.tags.map((tag) => (
+ #{tag}
+ ))}
+
+
+
+ {video.comments.map((item) => (
+
+ {item.text}
+ {new Date(item.createdAt).toLocaleString()}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
new file mode 100644
index 0000000..88ed866
--- /dev/null
+++ b/frontend/src/styles/global.css
@@ -0,0 +1,63 @@
+:root {
+ color-scheme: dark;
+ font-family: Inter, system-ui, sans-serif;
+ background: #050505;
+ color: #f5f5f5;
+}
+* { box-sizing: border-box; }
+body { margin: 0; background: #050505; color: #f5f5f5; }
+a { color: inherit; text-decoration: none; }
+button, input, textarea, select { font: inherit; }
+.app-shell { min-height: 100vh; background: linear-gradient(180deg, #080808 0%, #111 100%); }
+.site-header { position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: 1rem; padding: 1rem 1.5rem; background: rgba(0,0,0,.92); border-bottom: 1px solid #1f1f1f; }
+.brand { display: flex; align-items: center; gap: .75rem; font-size: 1.2rem; font-weight: 700; min-width: 170px; }
+.brand-logo { width: 42px; height: 42px; border-radius: 10px; object-fit: cover; }
+.search-input { flex: 1; background: #141414; border: 1px solid #2a2a2a; border-radius: 999px; color: #fff; padding: .8rem 1rem; }
+.nav-links { display: flex; gap: 1rem; }
+.nav-links a.active, .nav-links a:hover, .filter-bar button.active, .tag-bar button.active, .tag-link:hover { color: white; background: var(--primary); }
+.content { max-width: 1400px; margin: 0 auto; padding: 1.5rem; }
+.page-stack { display: flex; flex-direction: column; gap: 1.25rem; }
+.announcement, .chart-card, .admin-section, .player-panel, .related-panel, .overview-card { background: #111; border: 1px solid #1e1e1e; border-radius: 18px; padding: 1rem; box-shadow: 0 20px 40px rgba(0,0,0,.2); }
+.filter-bar, .tag-bar { display: flex; flex-wrap: wrap; gap: .75rem; }
+.filter-bar button, .tag-bar button, .tag-link, .admin-actions button, .comment-form button, .admin-form button { background: #181818; color: #d5d5d5; border: 1px solid #2b2b2b; border-radius: 999px; padding: .65rem 1rem; cursor: pointer; transition: .2s ease; }
+.video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
+.video-card { display: flex; flex-direction: column; background: #101010; border: 1px solid #1f1f1f; border-radius: 20px; overflow: hidden; transition: transform .25s ease, box-shadow .25s ease; }
+.video-card:hover { transform: translateY(-6px) scale(1.01); box-shadow: 0 18px 32px rgba(229,9,20,.15); }
+.thumbnail-wrap { aspect-ratio: 16 / 9; overflow: hidden; }
+.thumbnail { width: 100%; height: 100%; object-fit: cover; transition: transform .3s ease; }
+.video-card:hover .thumbnail { transform: scale(1.08); }
+.video-card-body { padding: .9rem; display: grid; gap: .35rem; }
+.video-card-body h3 { margin: 0; font-size: 1rem; }
+.video-card-body p, .video-card-body span, .video-meta, .comment-item span { color: #9f9f9f; margin: 0; }
+.detail-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr); gap: 1rem; }
+.video-player { width: 100%; max-height: 70vh; border: none; border-radius: 16px; background: #000; }
+.iframe-player { aspect-ratio: 16/9; min-height: 420px; }
+.related-card { display: grid; grid-template-columns: 120px 1fr; gap: .75rem; padding: .6rem 0; border-bottom: 1px solid #1f1f1f; }
+.related-card img { width: 120px; height: 68px; object-fit: cover; border-radius: 10px; }
+.comment-form, .admin-form { display: grid; gap: .85rem; }
+.comment-form textarea, .admin-form input, .admin-form textarea, .admin-form select { width: 100%; background: #181818; border: 1px solid #2a2a2a; border-radius: 14px; color: #fff; padding: .85rem 1rem; }
+.comment-list, .admin-list { display: grid; gap: .75rem; }
+.comment-item, .admin-item { background: #161616; border: 1px solid #242424; border-radius: 14px; padding: .85rem 1rem; }
+.admin-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
+.admin-actions { display: flex; gap: .5rem; }
+.admin-actions .danger { background: rgba(229,9,20,.15); color: #ff8c8c; }
+.overview-grid, .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }
+.overview-card strong { font-size: 2rem; margin-top: .4rem; display: block; }
+.ad-banner img { width: 100%; border-radius: 18px; display: block; }
+.popup-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.8); display: grid; place-items: center; z-index: 100; padding: 1rem; }
+.popup-card { position: relative; max-width: 900px; width: 100%; }
+.popup-card img { width: 100%; border-radius: 24px; display: block; }
+.popup-close { position: absolute; top: .75rem; right: .75rem; width: 42px; height: 42px; border-radius: 999px; border: none; background: rgba(0,0,0,.75); color: #fff; font-size: 1.5rem; cursor: pointer; }
+.skeleton { background: linear-gradient(90deg, #171717 25%, #222 50%, #171717 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
+.skeleton-card { display: grid; gap: .8rem; }
+.thumbnail-skeleton { aspect-ratio: 16/9; border-radius: 20px; }
+.line { height: 18px; border-radius: 999px; }
+.line.short { width: 60%; }
+.chart-skeleton { height: 280px; border-radius: 18px; }
+.checkbox-row { display: flex; align-items: center; justify-content: space-between; }
+@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
+@media (max-width: 900px) {
+ .site-header { flex-wrap: wrap; }
+ .detail-layout { grid-template-columns: 1fr; }
+ .iframe-player { min-height: 240px; }
+}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..efe6335
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173
+ }
+});
From 2cd83fad7cabbef86696f7acc8d41ad9dad03d57 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hi=E1=BA=BFu=20Star?=
<66201416+huzely@users.noreply.github.com>
Date: Thu, 19 Mar 2026 10:56:37 +0700
Subject: [PATCH 2/2] Rebuild site for cPanel with PHP and MySQL
---
README.md | 86 ++++----
admin/ads.php | 19 ++
admin/announcement.php | 17 ++
admin/index.php | 55 +++++
admin/login.php | 36 +++
admin/logout.php | 5 +
admin/partials/sidebar.php | 13 ++
admin/settings.php | 12 +
admin/taxonomy.php | 43 ++++
admin/videos.php | 22 ++
assets/css/style.css | 64 ++++++
assets/js/main.js | 23 ++
backend/.env.example | 3 -
backend/config/db.js | 11 -
backend/controllers/adController.js | 25 ---
backend/controllers/announcementController.js | 23 --
backend/controllers/commentController.js | 11 -
backend/controllers/metaController.js | 46 ----
backend/controllers/settingsController.js | 18 --
backend/controllers/statsController.js | 49 -----
backend/controllers/videoController.js | 118 ----------
backend/models/Ad.js | 13 --
backend/models/Announcement.js | 12 -
backend/models/Category.js | 11 -
backend/models/Comment.js | 11 -
backend/models/Setting.js | 13 --
backend/models/Tag.js | 10 -
backend/models/Video.js | 17 --
backend/models/ViewLog.js | 11 -
backend/package.json | 20 --
backend/routes/adRoutes.js | 9 -
backend/routes/announcementRoutes.js | 9 -
backend/routes/commentRoutes.js | 6 -
backend/routes/metaRoutes.js | 25 ---
backend/routes/searchRoutes.js | 8 -
backend/routes/settingsRoutes.js | 7 -
backend/routes/statsRoutes.js | 8 -
backend/routes/videoRoutes.js | 12 -
backend/seed/seedData.js | 56 -----
backend/server.js | 46 ----
database.sql | 126 +++++++++++
frontend/index.html | 12 -
frontend/package.json | 22 --
frontend/src/App.jsx | 111 ----------
frontend/src/api/client.js | 5 -
frontend/src/components/AdBanner.jsx | 9 -
frontend/src/components/Layout.jsx | 26 ---
frontend/src/components/PopupAd.jsx | 27 ---
frontend/src/components/Skeletons.jsx | 17 --
frontend/src/components/VideoCard.jsx | 16 --
frontend/src/main.jsx | 13 --
frontend/src/pages/AdminPage.jsx | 135 ------------
frontend/src/pages/HomePage.jsx | 47 ----
frontend/src/pages/UploadPage.jsx | 54 -----
frontend/src/pages/VideoDetailPage.jsx | 72 ------
frontend/src/styles/global.css | 63 ------
frontend/vite.config.js | 9 -
includes/config.php | 23 ++
includes/footer.php | 18 ++
includes/functions.php | 206 ++++++++++++++++++
includes/header.php | 48 ++++
public_html/index.php | 36 +++
public_html/search.php | 3 +
public_html/upload.php | 24 ++
public_html/video.php | 62 ++++++
65 files changed, 901 insertions(+), 1286 deletions(-)
create mode 100644 admin/ads.php
create mode 100644 admin/announcement.php
create mode 100644 admin/index.php
create mode 100644 admin/login.php
create mode 100644 admin/logout.php
create mode 100644 admin/partials/sidebar.php
create mode 100644 admin/settings.php
create mode 100644 admin/taxonomy.php
create mode 100644 admin/videos.php
create mode 100644 assets/css/style.css
create mode 100644 assets/js/main.js
delete mode 100644 backend/.env.example
delete mode 100644 backend/config/db.js
delete mode 100644 backend/controllers/adController.js
delete mode 100644 backend/controllers/announcementController.js
delete mode 100644 backend/controllers/commentController.js
delete mode 100644 backend/controllers/metaController.js
delete mode 100644 backend/controllers/settingsController.js
delete mode 100644 backend/controllers/statsController.js
delete mode 100644 backend/controllers/videoController.js
delete mode 100644 backend/models/Ad.js
delete mode 100644 backend/models/Announcement.js
delete mode 100644 backend/models/Category.js
delete mode 100644 backend/models/Comment.js
delete mode 100644 backend/models/Setting.js
delete mode 100644 backend/models/Tag.js
delete mode 100644 backend/models/Video.js
delete mode 100644 backend/models/ViewLog.js
delete mode 100644 backend/package.json
delete mode 100644 backend/routes/adRoutes.js
delete mode 100644 backend/routes/announcementRoutes.js
delete mode 100644 backend/routes/commentRoutes.js
delete mode 100644 backend/routes/metaRoutes.js
delete mode 100644 backend/routes/searchRoutes.js
delete mode 100644 backend/routes/settingsRoutes.js
delete mode 100644 backend/routes/statsRoutes.js
delete mode 100644 backend/routes/videoRoutes.js
delete mode 100644 backend/seed/seedData.js
delete mode 100644 backend/server.js
create mode 100644 database.sql
delete mode 100644 frontend/index.html
delete mode 100644 frontend/package.json
delete mode 100644 frontend/src/App.jsx
delete mode 100644 frontend/src/api/client.js
delete mode 100644 frontend/src/components/AdBanner.jsx
delete mode 100644 frontend/src/components/Layout.jsx
delete mode 100644 frontend/src/components/PopupAd.jsx
delete mode 100644 frontend/src/components/Skeletons.jsx
delete mode 100644 frontend/src/components/VideoCard.jsx
delete mode 100644 frontend/src/main.jsx
delete mode 100644 frontend/src/pages/AdminPage.jsx
delete mode 100644 frontend/src/pages/HomePage.jsx
delete mode 100644 frontend/src/pages/UploadPage.jsx
delete mode 100644 frontend/src/pages/VideoDetailPage.jsx
delete mode 100644 frontend/src/styles/global.css
delete mode 100644 frontend/vite.config.js
create mode 100644 includes/config.php
create mode 100644 includes/footer.php
create mode 100644 includes/functions.php
create mode 100644 includes/header.php
create mode 100644 public_html/index.php
create mode 100644 public_html/search.php
create mode 100644 public_html/upload.php
create mode 100644 public_html/video.php
diff --git a/README.md b/README.md
index 8f916f1..ce2b14b 100644
--- a/README.md
+++ b/README.md
@@ -1,58 +1,64 @@
-# NightFlix Fullstack Video Website
+# NightFlix PHP Video Website
-A complete fullstack video website built with React (Vite), Node.js/Express, and MongoDB. The project includes a public video browsing experience plus an admin dashboard for content, ads, announcements, analytics, and site settings.
+A shared-hosting-friendly video website built with pure PHP, MySQL, HTML, CSS, and JavaScript. It is structured for cPanel deployment and does not require Node.js.
-## Project structure
+## Folder structure
-- `backend/` – Express API, MongoDB models, seeding script, analytics endpoints
-- `frontend/` – React + Vite client with home page, detail page, upload page, and admin dashboard
+- `public_html/` - public pages (`index.php`, `video.php`, `upload.php`, `search.php`)
+- `admin/` - session-based admin dashboard and CRUD pages
+- `assets/` - CSS and JavaScript assets
+- `includes/` - configuration, helpers, shared header, shared footer
+- `uploads/` - reserved folder for future local file uploads
+- `database.sql` - MySQL schema and seed data
## Features
### Public site
-- Home page with responsive dark-mode video grid
+- Responsive dark mode homepage with YouTube-style video grid
- Search by title or tags
- Category and tag filters
-- Video detail page with HTML5 player or iframe embeds
-- Related videos
+- Video detail page with MP4 or iframe embed support
+- View counter and related videos
- Comment system
-- Popup ad, header/middle/footer banner ads
-- Announcement banner
-- Upload page
+- Upload form
+- Popup ad (once per session), banner ads, and announcement area
### Admin panel
-- Overview cards for total videos and total views
-- Daily and monthly analytics charts
-- CRUD for videos, categories, tags, ads, and announcements
-- Site settings editor for branding and popup ads toggle
+- Session-based login
+- Dashboard with total videos, total views, and daily/monthly statistics tables
+- Video CRUD
+- Category CRUD
+- Tag CRUD
+- Ads CRUD with enable/disable
+- Announcement edit/enable toggle
+- Site settings for logo, primary color, and popup ads
-## Run locally
+## Default admin account
-### 1) Backend
-```bash
-cd backend
-cp .env.example .env
-npm install
-npm run seed
-npm run dev
-```
+- Username: `admin`
+- Password: `admin123`
-Backend runs on `http://localhost:3000`.
+## Deploy on cPanel / shared hosting
-### 2) Frontend
-```bash
-cd frontend
-npm install
-npm run dev
-```
+1. Create a MySQL database and user in cPanel.
+2. Import `database.sql` using phpMyAdmin.
+3. Upload the project files so that these folders exist in your hosting account:
+ - `public_html/`
+ - `admin/`
+ - `assets/`
+ - `includes/`
+ - `uploads/`
+4. Edit `includes/config.php` and update:
+ - `DB_HOST`
+ - `DB_NAME`
+ - `DB_USER`
+ - `DB_PASS`
+ - `BASE_URL` if your site is installed in a subdirectory
+5. Open `public_html/index.php` in the browser.
+6. Open `admin/login.php` to access the admin dashboard.
-Frontend runs on `http://localhost:5173`.
+## Notes
-## MongoDB
-Make sure MongoDB is running locally at the URI defined in `backend/.env`.
-
-Default local URI:
-
-```bash
-mongodb://127.0.0.1:27017/video_site
-```
+- This project uses PDO and PHP sessions, which are supported on standard cPanel hosting.
+- The popup ad is controlled by JavaScript and `sessionStorage`.
+- `uploads/` is included for future file upload storage if you later switch from URL-based media to hosted files.
diff --git a/admin/ads.php b/admin/ads.php
new file mode 100644
index 0000000..a77c123
--- /dev/null
+++ b/admin/ads.php
@@ -0,0 +1,19 @@
+prepare('UPDATE ads SET image=:image, link=:link, position=:position, active=:active WHERE id=:id');
+ $stmt->execute(['image' => trim($_POST['image']), 'link' => trim($_POST['link']), 'position' => trim($_POST['position']), 'active' => !empty($_POST['active']) ? 1 : 0, 'id' => (int) $_POST['id']]);
+ } else {
+ $stmt = $pdo->prepare('INSERT INTO ads (image, link, position, active) VALUES (:image, :link, :position, :active)');
+ $stmt->execute(['image' => trim($_POST['image']), 'link' => trim($_POST['link']), 'position' => trim($_POST['position']), 'active' => !empty($_POST['active']) ? 1 : 0]);
+ }
+ header('Location: ads.php'); exit;
+}
+if (isset($_GET['delete'])) { $pdo->prepare('DELETE FROM ads WHERE id=:id')->execute(['id' => (int) $_GET['delete']]); header('Location: ads.php'); exit; }
+$editing = null;
+if (isset($_GET['edit'])) { $editing = $pdo->prepare('SELECT * FROM ads WHERE id=:id'); $editing->execute(['id' => (int) $_GET['edit']]); $editing = $editing->fetch(); }
+$ads = get_ads(null, false);
+?>
+Ads
diff --git a/admin/announcement.php b/admin/announcement.php
new file mode 100644
index 0000000..dd08a5c
--- /dev/null
+++ b/admin/announcement.php
@@ -0,0 +1,17 @@
+query('SELECT id FROM announcements ORDER BY id DESC LIMIT 1')->fetchColumn();
+ if ($current) {
+ $stmt = $pdo->prepare('UPDATE announcements SET title=:title, content=:content, active=:active WHERE id=:id');
+ $stmt->execute(['title' => trim($_POST['title']), 'content' => trim($_POST['content']), 'active' => !empty($_POST['active']) ? 1 : 0, 'id' => $current]);
+ } else {
+ $stmt = $pdo->prepare('INSERT INTO announcements (title, content, active) VALUES (:title, :content, :active)');
+ $stmt->execute(['title' => trim($_POST['title']), 'content' => trim($_POST['content']), 'active' => !empty($_POST['active']) ? 1 : 0]);
+ }
+ header('Location: announcement.php'); exit;
+}
+$announcement = $pdo->query('SELECT * FROM announcements ORDER BY id DESC LIMIT 1')->fetch();
+?>
+Announcement
diff --git a/admin/index.php b/admin/index.php
new file mode 100644
index 0000000..b3ff6b1
--- /dev/null
+++ b/admin/index.php
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+ Admin Dashboard
+
+
+
+
+
+
+
+ Total Videos
+ Total Views
+
+
+
+
+ Quick Overview
+ Videos: · Categories: · Tags: · Ads: · Announcement: · Primary color:
+
+
+
+
+
diff --git a/admin/login.php b/admin/login.php
new file mode 100644
index 0000000..6c1dbb7
--- /dev/null
+++ b/admin/login.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Admin Login
+
+
+
+
+
+
diff --git a/admin/logout.php b/admin/logout.php
new file mode 100644
index 0000000..edf7460
--- /dev/null
+++ b/admin/logout.php
@@ -0,0 +1,5 @@
+
+ Admin Panel
+ Logged in as
+
+
diff --git a/admin/settings.php b/admin/settings.php
new file mode 100644
index 0000000..f9c4db0
--- /dev/null
+++ b/admin/settings.php
@@ -0,0 +1,12 @@
+prepare('UPDATE settings SET site_name=:site_name, logo=:logo, primary_color=:primary_color, popup_ads_enabled=:popup_ads_enabled WHERE id=:id');
+ $stmt->execute(['site_name' => trim($_POST['site_name']), 'logo' => trim($_POST['logo']), 'primary_color' => trim($_POST['primary_color']), 'popup_ads_enabled' => !empty($_POST['popup_ads_enabled']) ? 1 : 0, 'id' => $current['id']]);
+ header('Location: settings.php'); exit;
+}
+$settings = site_settings();
+?>
+Settings
diff --git a/admin/taxonomy.php b/admin/taxonomy.php
new file mode 100644
index 0000000..73483dd
--- /dev/null
+++ b/admin/taxonomy.php
@@ -0,0 +1,43 @@
+prepare('SELECT * FROM categories WHERE id=:id');
+ $stmt->execute(['id' => (int) $_GET['edit_category']]);
+ $editCategory = $stmt->fetch();
+}
+if (isset($_GET['edit_tag'])) {
+ $stmt = $pdo->prepare('SELECT * FROM tags WHERE id=:id');
+ $stmt->execute(['id' => (int) $_GET['edit_tag']]);
+ $editTag = $stmt->fetch();
+}
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if ($_POST['entity'] === 'category') {
+ if (!empty($_POST['id'])) {
+ $stmt = $pdo->prepare('UPDATE categories SET name=:name, description=:description WHERE id=:id');
+ $stmt->execute(['name' => trim($_POST['name']), 'description' => trim($_POST['description']), 'id' => (int) $_POST['id']]);
+ } else {
+ $stmt = $pdo->prepare('INSERT INTO categories (name, description) VALUES (:name, :description)');
+ $stmt->execute(['name' => trim($_POST['name']), 'description' => trim($_POST['description'])]);
+ }
+ }
+ if ($_POST['entity'] === 'tag') {
+ if (!empty($_POST['id'])) {
+ $stmt = $pdo->prepare('UPDATE tags SET name=:name WHERE id=:id');
+ $stmt->execute(['name' => trim($_POST['name']), 'id' => (int) $_POST['id']]);
+ } else {
+ $stmt = $pdo->prepare('INSERT INTO tags (name) VALUES (:name)');
+ $stmt->execute(['name' => trim($_POST['name'])]);
+ }
+ }
+ header('Location: taxonomy.php');
+ exit;
+}
+if (isset($_GET['delete_category'])) { $pdo->prepare('DELETE FROM categories WHERE id=:id')->execute(['id' => (int) $_GET['delete_category']]); header('Location: taxonomy.php'); exit; }
+if (isset($_GET['delete_tag'])) { $pdo->prepare('DELETE FROM tags WHERE id=:id')->execute(['id' => (int) $_GET['delete_tag']]); header('Location: taxonomy.php'); exit; }
+$categories = get_categories();
+$tags = get_tags();
+?>
+Taxonomy
diff --git a/admin/videos.php b/admin/videos.php
new file mode 100644
index 0000000..8dc837f
--- /dev/null
+++ b/admin/videos.php
@@ -0,0 +1,22 @@
+prepare('DELETE FROM videos WHERE id = :id');
+ $stmt->execute(['id' => (int) $_GET['delete']]);
+ header('Location: videos.php');
+ exit;
+}
+$editing = null;
+if (isset($_GET['edit'])) {
+ $editing = fetch_video((int) $_GET['edit']);
+}
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $id = !empty($_POST['id']) ? (int) $_POST['id'] : null;
+ save_video($_POST, $id);
+ header('Location: videos.php');
+ exit;
+}
+$videos = fetch_videos();
+?>
+Manage Videos
diff --git a/assets/css/style.css b/assets/css/style.css
new file mode 100644
index 0000000..f8f0040
--- /dev/null
+++ b/assets/css/style.css
@@ -0,0 +1,64 @@
+:root {
+ --primary-color: #e50914;
+ --bg: #050505;
+ --panel: #111111;
+ --border: #262626;
+ --muted: #aaaaaa;
+ --text: #f6f6f6;
+ color-scheme: dark;
+}
+* { box-sizing: border-box; }
+body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: var(--bg); color: var(--text); }
+a { color: inherit; text-decoration: none; }
+img { max-width: 100%; display: block; }
+.container { width: min(1280px, calc(100% - 2rem)); margin: 0 auto; }
+.site-header { position: sticky; top: 0; z-index: 30; background: rgba(0,0,0,.96); border-bottom: 1px solid var(--border); }
+.nav-row { display: flex; align-items: center; gap: 1rem; padding: 1rem 0; }
+.brand { display: flex; align-items: center; gap: .75rem; font-size: 1.2rem; font-weight: 700; }
+.brand-logo { width: 42px; height: 42px; border-radius: 10px; object-fit: cover; }
+.search-form { flex: 1; }
+.search-form input, .admin-form input, .admin-form textarea, .admin-form select, .comment-form textarea { width: 100%; background: #161616; color: #fff; border: 1px solid var(--border); border-radius: 14px; padding: .9rem 1rem; }
+.top-links, .chip-row, .admin-nav { display: flex; flex-wrap: wrap; gap: .75rem; }
+.top-links a, .chip, .btn-primary, .admin-nav a { padding: .7rem 1rem; border-radius: 999px; border: 1px solid var(--border); background: #181818; transition: .2s ease; }
+.top-links a:hover, .chip:hover, .chip.active, .btn-primary, .admin-nav a:hover { background: var(--primary-color); color: #fff; }
+.main-content, .admin-main { display: grid; gap: 1rem; padding: 1.25rem 0 2rem; }
+.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 20px; padding: 1rem; box-shadow: 0 20px 40px rgba(0,0,0,.18); }
+.nested-panel { background: #151515; }
+.banner-ad img { border-radius: 18px; }
+.video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
+.video-card { background: #101010; border: 1px solid var(--border); border-radius: 20px; overflow: hidden; transition: transform .25s ease, box-shadow .25s ease; }
+.video-card:hover { transform: translateY(-6px); box-shadow: 0 18px 30px rgba(229,9,20,.18); }
+.thumb-wrap { aspect-ratio: 16 / 9; overflow: hidden; }
+.thumb-wrap img { width: 100%; height: 100%; object-fit: cover; transition: transform .3s ease; }
+.video-card:hover .thumb-wrap img { transform: scale(1.08); }
+.video-meta-card { padding: .95rem; display: grid; gap: .35rem; }
+.video-meta-card h3, .related-card h3, .panel h1, .panel h2 { margin: 0; }
+.video-meta-card p, .video-meta-card span, .muted { color: var(--muted); }
+.filters { display: grid; gap: .75rem; }
+.detail-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); gap: 1rem; }
+.player { width: 100%; border: 0; border-radius: 18px; background: #000; min-height: 420px; }
+.related-card { display: grid; grid-template-columns: 120px 1fr; gap: .75rem; padding: .75rem 0; border-bottom: 1px solid var(--border); }
+.related-card img { width: 120px; height: 68px; object-fit: cover; border-radius: 10px; }
+.comment-form, .admin-form, .form-panel { display: grid; gap: .9rem; }
+.comment-list { display: grid; gap: .75rem; }
+.comment-item { background: #161616; border-radius: 14px; padding: .85rem 1rem; border: 1px solid var(--border); }
+.popup-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.82); display: grid; place-items: center; padding: 1rem; }
+.popup-card { position: relative; max-width: 920px; width: 100%; }
+.popup-card img { border-radius: 24px; }
+.popup-close { position: absolute; top: .75rem; right: .75rem; width: 42px; height: 42px; border: 0; border-radius: 999px; background: rgba(0,0,0,.8); color: #fff; font-size: 1.4rem; cursor: pointer; }
+.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 1rem; }
+.login-card { width: min(460px, 100%); }
+.admin-layout { display: grid; grid-template-columns: 280px 1fr; gap: 1rem; width: min(1380px, calc(100% - 2rem)); margin: 1rem auto; }
+.admin-sidebar { height: fit-content; position: sticky; top: 1rem; }
+.stats-grid, .split-panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }
+.stat-card strong { display: block; font-size: 2rem; margin-top: .4rem; }
+.data-table { width: 100%; border-collapse: collapse; }
+.data-table th, .data-table td { padding: .85rem; border-bottom: 1px solid var(--border); text-align: left; }
+.inline-check { display: flex; align-items: center; gap: .65rem; }
+.success { color: #89ffaf; }
+.error { color: #ff9898; }
+@media (max-width: 900px) {
+ .nav-row, .admin-layout, .detail-layout { grid-template-columns: 1fr; display: grid; }
+ .top-links { justify-content: flex-start; }
+ .player { min-height: 260px; }
+}
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 0000000..c2174c6
--- /dev/null
+++ b/assets/js/main.js
@@ -0,0 +1,23 @@
+document.addEventListener('DOMContentLoaded', function () {
+ var popup = document.getElementById('popupAd');
+ if (popup && !sessionStorage.getItem('popupSeen')) {
+ popup.hidden = false;
+ sessionStorage.setItem('popupSeen', '1');
+ }
+
+ document.querySelectorAll('[data-close-popup]').forEach(function (button) {
+ button.addEventListener('click', function () {
+ if (popup) {
+ popup.hidden = true;
+ }
+ });
+ });
+
+ if (popup) {
+ popup.addEventListener('click', function (event) {
+ if (event.target === popup) {
+ popup.hidden = true;
+ }
+ });
+ }
+});
diff --git a/backend/.env.example b/backend/.env.example
deleted file mode 100644
index 54a20f3..0000000
--- a/backend/.env.example
+++ /dev/null
@@ -1,3 +0,0 @@
-PORT=3000
-MONGODB_URI=mongodb://127.0.0.1:27017/video_site
-FRONTEND_URL=http://localhost:5173
diff --git a/backend/config/db.js b/backend/config/db.js
deleted file mode 100644
index 3b9a7b9..0000000
--- a/backend/config/db.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import mongoose from 'mongoose';
-
-export async function connectDB(uri) {
- try {
- await mongoose.connect(uri);
- console.log('MongoDB connected');
- } catch (error) {
- console.error('MongoDB connection error:', error.message);
- process.exit(1);
- }
-}
diff --git a/backend/controllers/adController.js b/backend/controllers/adController.js
deleted file mode 100644
index c4c4e59..0000000
--- a/backend/controllers/adController.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Ad } from '../models/Ad.js';
-
-export async function getAds(req, res) {
- const filter = {};
- if (req.query.active === 'true') filter.active = true;
- const ads = await Ad.find(filter).sort({ createdAt: -1 });
- res.json(ads);
-}
-
-export async function createAd(req, res) {
- const ad = await Ad.create(req.body);
- res.status(201).json(ad);
-}
-
-export async function updateAd(req, res) {
- const ad = await Ad.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
- if (!ad) return res.status(404).json({ message: 'Ad not found' });
- res.json(ad);
-}
-
-export async function deleteAd(req, res) {
- const ad = await Ad.findByIdAndDelete(req.params.id);
- if (!ad) return res.status(404).json({ message: 'Ad not found' });
- res.json({ message: 'Ad deleted' });
-}
diff --git a/backend/controllers/announcementController.js b/backend/controllers/announcementController.js
deleted file mode 100644
index 240e36f..0000000
--- a/backend/controllers/announcementController.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Announcement } from '../models/Announcement.js';
-
-export async function getAnnouncement(req, res) {
- const announcement = await Announcement.findOne({ active: true }).sort({ updatedAt: -1 });
- res.json(announcement);
-}
-
-export async function createAnnouncement(req, res) {
- const announcement = await Announcement.create(req.body);
- res.status(201).json(announcement);
-}
-
-export async function updateAnnouncement(req, res) {
- const announcement = await Announcement.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
- if (!announcement) return res.status(404).json({ message: 'Announcement not found' });
- res.json(announcement);
-}
-
-export async function deleteAnnouncement(req, res) {
- const announcement = await Announcement.findByIdAndDelete(req.params.id);
- if (!announcement) return res.status(404).json({ message: 'Announcement not found' });
- res.json({ message: 'Announcement deleted' });
-}
diff --git a/backend/controllers/commentController.js b/backend/controllers/commentController.js
deleted file mode 100644
index 85d951d..0000000
--- a/backend/controllers/commentController.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Comment } from '../models/Comment.js';
-
-export async function createComment(req, res) {
- const { videoId, text } = req.body;
- if (!videoId || !text?.trim()) {
- return res.status(400).json({ message: 'videoId and text are required' });
- }
-
- const comment = await Comment.create({ videoId, text: text.trim() });
- res.status(201).json(comment);
-}
diff --git a/backend/controllers/metaController.js b/backend/controllers/metaController.js
deleted file mode 100644
index 50a24b1..0000000
--- a/backend/controllers/metaController.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Category } from '../models/Category.js';
-import { Tag } from '../models/Tag.js';
-
-export async function listCategories(req, res) {
- const categories = await Category.find().sort({ name: 1 });
- res.json(categories);
-}
-
-export async function createCategory(req, res) {
- const category = await Category.create(req.body);
- res.status(201).json(category);
-}
-
-export async function updateCategory(req, res) {
- const category = await Category.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
- if (!category) return res.status(404).json({ message: 'Category not found' });
- res.json(category);
-}
-
-export async function deleteCategory(req, res) {
- const category = await Category.findByIdAndDelete(req.params.id);
- if (!category) return res.status(404).json({ message: 'Category not found' });
- res.json({ message: 'Category deleted' });
-}
-
-export async function listTags(req, res) {
- const tags = await Tag.find().sort({ name: 1 });
- res.json(tags);
-}
-
-export async function createTag(req, res) {
- const tag = await Tag.create(req.body);
- res.status(201).json(tag);
-}
-
-export async function updateTag(req, res) {
- const tag = await Tag.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
- if (!tag) return res.status(404).json({ message: 'Tag not found' });
- res.json(tag);
-}
-
-export async function deleteTag(req, res) {
- const tag = await Tag.findByIdAndDelete(req.params.id);
- if (!tag) return res.status(404).json({ message: 'Tag not found' });
- res.json({ message: 'Tag deleted' });
-}
diff --git a/backend/controllers/settingsController.js b/backend/controllers/settingsController.js
deleted file mode 100644
index 9ee7c31..0000000
--- a/backend/controllers/settingsController.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Setting } from '../models/Setting.js';
-
-export async function getSettings(req, res) {
- const settings = await Setting.findOne().sort({ updatedAt: -1 });
- res.json(settings);
-}
-
-export async function saveSettings(req, res) {
- const existing = await Setting.findOne();
- if (existing) {
- Object.assign(existing, req.body);
- await existing.save();
- return res.json(existing);
- }
-
- const settings = await Setting.create(req.body);
- res.status(201).json(settings);
-}
diff --git a/backend/controllers/statsController.js b/backend/controllers/statsController.js
deleted file mode 100644
index 080892d..0000000
--- a/backend/controllers/statsController.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Video } from '../models/Video.js';
-import { ViewLog } from '../models/ViewLog.js';
-
-export async function getOverview(req, res) {
- const [totalVideos, totalViewsAgg] = await Promise.all([
- Video.countDocuments(),
- Video.aggregate([{ $group: { _id: null, totalViews: { $sum: '$views' } } }])
- ]);
-
- res.json({
- totalVideos,
- totalViews: totalViewsAgg[0]?.totalViews || 0
- });
-}
-
-export async function getDaily(req, res) {
- const data = await ViewLog.aggregate([
- {
- $group: {
- _id: {
- year: { $year: '$createdAt' },
- month: { $month: '$createdAt' },
- day: { $dayOfMonth: '$createdAt' }
- },
- count: { $sum: 1 }
- }
- },
- { $sort: { '_id.year': 1, '_id.month': 1, '_id.day': 1 } }
- ]);
-
- res.json(data.map((item) => ({ date: `${item._id.year}-${String(item._id.month).padStart(2, '0')}-${String(item._id.day).padStart(2, '0')}`, count: item.count })));
-}
-
-export async function getMonthly(req, res) {
- const data = await ViewLog.aggregate([
- {
- $group: {
- _id: {
- year: { $year: '$createdAt' },
- month: { $month: '$createdAt' }
- },
- count: { $sum: 1 }
- }
- },
- { $sort: { '_id.year': 1, '_id.month': 1 } }
- ]);
-
- res.json(data.map((item) => ({ month: `${item._id.year}-${String(item._id.month).padStart(2, '0')}`, count: item.count })));
-}
diff --git a/backend/controllers/videoController.js b/backend/controllers/videoController.js
deleted file mode 100644
index 87560e0..0000000
--- a/backend/controllers/videoController.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import mongoose from 'mongoose';
-import { Video } from '../models/Video.js';
-import { Comment } from '../models/Comment.js';
-import { ViewLog } from '../models/ViewLog.js';
-import { Tag } from '../models/Tag.js';
-import { Category } from '../models/Category.js';
-
-function normalizeTags(tags) {
- if (Array.isArray(tags)) return tags.map((tag) => String(tag).trim()).filter(Boolean);
- if (typeof tags === 'string') {
- return tags.split(',').map((tag) => tag.trim()).filter(Boolean);
- }
- return [];
-}
-
-async function syncMetadata(category, tags) {
- if (category) {
- await Category.findOneAndUpdate(
- { name: category.trim() },
- { $setOnInsert: { name: category.trim() } },
- { upsert: true, new: true }
- );
- }
-
- if (tags.length) {
- await Promise.all(
- tags.map((name) =>
- Tag.findOneAndUpdate({ name }, { $setOnInsert: { name } }, { upsert: true, new: true })
- )
- );
- }
-}
-
-export async function getVideos(req, res) {
- const { category, tag, q } = req.query;
- const filter = {};
-
- if (category) filter.category = category;
- if (tag) filter.tags = tag;
- if (q) {
- filter.$or = [
- { title: { $regex: q, $options: 'i' } },
- { tags: { $elemMatch: { $regex: q, $options: 'i' } } }
- ];
- }
-
- const videos = await Video.find(filter).sort({ createdAt: -1 });
- res.json(videos);
-}
-
-export async function getVideoById(req, res) {
- const { id } = req.params;
- if (!mongoose.Types.ObjectId.isValid(id)) {
- return res.status(400).json({ message: 'Invalid video id' });
- }
-
- const video = await Video.findById(id);
- if (!video) {
- return res.status(404).json({ message: 'Video not found' });
- }
-
- video.views += 1;
- await video.save();
-
- await ViewLog.create({
- videoId: video._id,
- ip: req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || 'unknown'
- });
-
- const comments = await Comment.find({ videoId: video._id }).sort({ createdAt: -1 });
- const relatedVideos = await Video.find({
- _id: { $ne: video._id },
- $or: [{ category: video.category }, { tags: { $in: video.tags } }]
- })
- .limit(8)
- .sort({ views: -1, createdAt: -1 });
-
- return res.json({ ...video.toObject(), comments, relatedVideos });
-}
-
-export async function createVideo(req, res) {
- const payload = req.body;
- const tags = normalizeTags(payload.tags);
- const video = await Video.create({ ...payload, tags });
- await syncMetadata(video.category, tags);
- res.status(201).json(video);
-}
-
-export async function updateVideo(req, res) {
- const { id } = req.params;
- const payload = req.body;
- const tags = normalizeTags(payload.tags);
- const video = await Video.findByIdAndUpdate(id, { ...payload, tags }, { new: true, runValidators: true });
- if (!video) return res.status(404).json({ message: 'Video not found' });
- await syncMetadata(video.category, tags);
- res.json(video);
-}
-
-export async function deleteVideo(req, res) {
- const { id } = req.params;
- const video = await Video.findByIdAndDelete(id);
- if (!video) return res.status(404).json({ message: 'Video not found' });
- await Comment.deleteMany({ videoId: id });
- await ViewLog.deleteMany({ videoId: id });
- res.json({ message: 'Video deleted' });
-}
-
-export async function searchVideos(req, res) {
- const q = req.query.q || '';
- const videos = await Video.find({
- $or: [
- { title: { $regex: q, $options: 'i' } },
- { tags: { $elemMatch: { $regex: q, $options: 'i' } } }
- ]
- }).sort({ createdAt: -1 });
-
- res.json(videos);
-}
diff --git a/backend/models/Ad.js b/backend/models/Ad.js
deleted file mode 100644
index cad855b..0000000
--- a/backend/models/Ad.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import mongoose from 'mongoose';
-
-const adSchema = new mongoose.Schema(
- {
- image: { type: String, required: true },
- link: { type: String, required: true },
- position: { type: String, enum: ['popup', 'header', 'middle', 'footer'], required: true },
- active: { type: Boolean, default: true }
- },
- { timestamps: true }
-);
-
-export const Ad = mongoose.model('Ad', adSchema);
diff --git a/backend/models/Announcement.js b/backend/models/Announcement.js
deleted file mode 100644
index ccf9792..0000000
--- a/backend/models/Announcement.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import mongoose from 'mongoose';
-
-const announcementSchema = new mongoose.Schema(
- {
- title: { type: String, required: true },
- content: { type: String, required: true },
- active: { type: Boolean, default: true }
- },
- { timestamps: true }
-);
-
-export const Announcement = mongoose.model('Announcement', announcementSchema);
diff --git a/backend/models/Category.js b/backend/models/Category.js
deleted file mode 100644
index 2b4bc60..0000000
--- a/backend/models/Category.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import mongoose from 'mongoose';
-
-const categorySchema = new mongoose.Schema(
- {
- name: { type: String, required: true, unique: true, trim: true },
- description: { type: String, default: '' }
- },
- { timestamps: true }
-);
-
-export const Category = mongoose.model('Category', categorySchema);
diff --git a/backend/models/Comment.js b/backend/models/Comment.js
deleted file mode 100644
index 44be812..0000000
--- a/backend/models/Comment.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import mongoose from 'mongoose';
-
-const commentSchema = new mongoose.Schema(
- {
- videoId: { type: mongoose.Schema.Types.ObjectId, ref: 'Video', required: true },
- text: { type: String, required: true, trim: true }
- },
- { timestamps: true }
-);
-
-export const Comment = mongoose.model('Comment', commentSchema);
diff --git a/backend/models/Setting.js b/backend/models/Setting.js
deleted file mode 100644
index 9f6dc30..0000000
--- a/backend/models/Setting.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import mongoose from 'mongoose';
-
-const settingSchema = new mongoose.Schema(
- {
- siteName: { type: String, default: 'NightFlix' },
- logo: { type: String, default: '' },
- primaryColor: { type: String, default: '#e50914' },
- popupAdsEnabled: { type: Boolean, default: true }
- },
- { timestamps: true }
-);
-
-export const Setting = mongoose.model('Setting', settingSchema);
diff --git a/backend/models/Tag.js b/backend/models/Tag.js
deleted file mode 100644
index 2eb4824..0000000
--- a/backend/models/Tag.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import mongoose from 'mongoose';
-
-const tagSchema = new mongoose.Schema(
- {
- name: { type: String, required: true, unique: true, trim: true }
- },
- { timestamps: true }
-);
-
-export const Tag = mongoose.model('Tag', tagSchema);
diff --git a/backend/models/Video.js b/backend/models/Video.js
deleted file mode 100644
index da2074d..0000000
--- a/backend/models/Video.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import mongoose from 'mongoose';
-
-const videoSchema = new mongoose.Schema(
- {
- title: { type: String, required: true, trim: true },
- description: { type: String, default: '' },
- thumbnail: { type: String, required: true },
- videoUrl: { type: String, required: true },
- type: { type: String, enum: ['mp4', 'embed'], required: true },
- views: { type: Number, default: 0 },
- tags: [{ type: String, trim: true }],
- category: { type: String, required: true, trim: true }
- },
- { timestamps: true }
-);
-
-export const Video = mongoose.model('Video', videoSchema);
diff --git a/backend/models/ViewLog.js b/backend/models/ViewLog.js
deleted file mode 100644
index 2bb05e7..0000000
--- a/backend/models/ViewLog.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import mongoose from 'mongoose';
-
-const viewLogSchema = new mongoose.Schema(
- {
- videoId: { type: mongoose.Schema.Types.ObjectId, ref: 'Video', required: true },
- ip: { type: String, default: 'unknown' }
- },
- { timestamps: true }
-);
-
-export const ViewLog = mongoose.model('ViewLog', viewLogSchema);
diff --git a/backend/package.json b/backend/package.json
deleted file mode 100644
index b437b40..0000000
--- a/backend/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "video-site-backend",
- "version": "1.0.0",
- "type": "module",
- "scripts": {
- "dev": "nodemon server.js",
- "start": "node server.js",
- "seed": "node seed/seedData.js"
- },
- "dependencies": {
- "cors": "^2.8.5",
- "dotenv": "^16.4.5",
- "express": "^4.19.2",
- "mongoose": "^8.6.1",
- "morgan": "^1.10.0"
- },
- "devDependencies": {
- "nodemon": "^3.1.4"
- }
-}
diff --git a/backend/routes/adRoutes.js b/backend/routes/adRoutes.js
deleted file mode 100644
index a6c085e..0000000
--- a/backend/routes/adRoutes.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Router } from 'express';
-import { createAd, deleteAd, getAds, updateAd } from '../controllers/adController.js';
-
-const router = Router();
-router.get('/', getAds);
-router.post('/', createAd);
-router.put('/:id', updateAd);
-router.delete('/:id', deleteAd);
-export default router;
diff --git a/backend/routes/announcementRoutes.js b/backend/routes/announcementRoutes.js
deleted file mode 100644
index aee8ca5..0000000
--- a/backend/routes/announcementRoutes.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Router } from 'express';
-import { createAnnouncement, deleteAnnouncement, getAnnouncement, updateAnnouncement } from '../controllers/announcementController.js';
-
-const router = Router();
-router.get('/', getAnnouncement);
-router.post('/', createAnnouncement);
-router.put('/:id', updateAnnouncement);
-router.delete('/:id', deleteAnnouncement);
-export default router;
diff --git a/backend/routes/commentRoutes.js b/backend/routes/commentRoutes.js
deleted file mode 100644
index 4027a3f..0000000
--- a/backend/routes/commentRoutes.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Router } from 'express';
-import { createComment } from '../controllers/commentController.js';
-
-const router = Router();
-router.post('/', createComment);
-export default router;
diff --git a/backend/routes/metaRoutes.js b/backend/routes/metaRoutes.js
deleted file mode 100644
index ba57116..0000000
--- a/backend/routes/metaRoutes.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Router } from 'express';
-import {
- createCategory,
- createTag,
- deleteCategory,
- deleteTag,
- listCategories,
- listTags,
- updateCategory,
- updateTag
-} from '../controllers/metaController.js';
-
-const categoryRouter = Router();
-categoryRouter.get('/', listCategories);
-categoryRouter.post('/', createCategory);
-categoryRouter.put('/:id', updateCategory);
-categoryRouter.delete('/:id', deleteCategory);
-
-const tagRouter = Router();
-tagRouter.get('/', listTags);
-tagRouter.post('/', createTag);
-tagRouter.put('/:id', updateTag);
-tagRouter.delete('/:id', deleteTag);
-
-export { categoryRouter, tagRouter };
diff --git a/backend/routes/searchRoutes.js b/backend/routes/searchRoutes.js
deleted file mode 100644
index 8983fdc..0000000
--- a/backend/routes/searchRoutes.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Router } from 'express';
-import { searchVideos } from '../controllers/videoController.js';
-
-const router = Router();
-
-router.get('/', searchVideos);
-
-export default router;
diff --git a/backend/routes/settingsRoutes.js b/backend/routes/settingsRoutes.js
deleted file mode 100644
index c325434..0000000
--- a/backend/routes/settingsRoutes.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Router } from 'express';
-import { getSettings, saveSettings } from '../controllers/settingsController.js';
-
-const router = Router();
-router.get('/', getSettings);
-router.post('/', saveSettings);
-export default router;
diff --git a/backend/routes/statsRoutes.js b/backend/routes/statsRoutes.js
deleted file mode 100644
index 4484775..0000000
--- a/backend/routes/statsRoutes.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Router } from 'express';
-import { getDaily, getMonthly, getOverview } from '../controllers/statsController.js';
-
-const router = Router();
-router.get('/overview', getOverview);
-router.get('/daily', getDaily);
-router.get('/monthly', getMonthly);
-export default router;
diff --git a/backend/routes/videoRoutes.js b/backend/routes/videoRoutes.js
deleted file mode 100644
index 2f002f4..0000000
--- a/backend/routes/videoRoutes.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Router } from 'express';
-import { createVideo, deleteVideo, getVideoById, getVideos, searchVideos, updateVideo } from '../controllers/videoController.js';
-
-const router = Router();
-
-router.get('/', getVideos);
-router.get('/:id', getVideoById);
-router.post('/', createVideo);
-router.put('/:id', updateVideo);
-router.delete('/:id', deleteVideo);
-
-export default router;
diff --git a/backend/seed/seedData.js b/backend/seed/seedData.js
deleted file mode 100644
index 6ea6dc5..0000000
--- a/backend/seed/seedData.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import 'dotenv/config';
-import { connectDB } from '../config/db.js';
-import { Video } from '../models/Video.js';
-import { Category } from '../models/Category.js';
-import { Tag } from '../models/Tag.js';
-import { Ad } from '../models/Ad.js';
-import { Announcement } from '../models/Announcement.js';
-import { Setting } from '../models/Setting.js';
-
-const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/video_site';
-
-const categories = ['Trending', 'Featured', 'Action', 'Drama', 'Music'];
-const tags = ['hot', 'hd', 'exclusive', 'trending', 'viral', 'night', 'cinema'];
-const videos = Array.from({ length: 10 }).map((_, index) => ({
- title: `Sample Video ${index + 1}`,
- description: `Demo description for sample video ${index + 1}.`,
- thumbnail: `https://picsum.photos/seed/video-${index + 1}/640/360`,
- videoUrl: index % 2 === 0 ? 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4' : 'https://www.youtube.com/embed/dQw4w9WgXcQ',
- type: index % 2 === 0 ? 'mp4' : 'embed',
- views: 1000 + index * 137,
- tags: [tags[index % tags.length], tags[(index + 2) % tags.length]],
- category: categories[index % categories.length],
- createdAt: new Date(Date.now() - index * 86400000)
-}));
-
-const ads = [
- { image: 'https://picsum.photos/seed/header-ad/1200/180', link: 'https://example.com/header', position: 'header', active: true },
- { image: 'https://picsum.photos/seed/middle-ad/1200/180', link: 'https://example.com/middle', position: 'middle', active: true },
- { image: 'https://picsum.photos/seed/footer-ad/1200/180', link: 'https://example.com/footer', position: 'footer', active: true },
- { image: 'https://picsum.photos/seed/popup-ad/1280/720', link: 'https://example.com/popup', position: 'popup', active: true }
-];
-
-async function seed() {
- await connectDB(MONGODB_URI);
-
- await Promise.all([
- Video.deleteMany({}),
- Category.deleteMany({}),
- Tag.deleteMany({}),
- Ad.deleteMany({}),
- Announcement.deleteMany({}),
- Setting.deleteMany({})
- ]);
-
- await Category.insertMany(categories.map((name) => ({ name })));
- await Tag.insertMany(tags.map((name) => ({ name })));
- await Video.insertMany(videos);
- await Ad.insertMany(ads);
- await Announcement.create({ title: 'Tonight\'s Featured Drop', content: 'New videos are live now. Browse the latest uploads and trending picks.', active: true });
- await Setting.create({ siteName: 'NightFlix', logo: 'https://picsum.photos/seed/logo/120/40', primaryColor: '#e50914', popupAdsEnabled: true });
-
- console.log('Seed complete');
- process.exit(0);
-}
-
-seed();
diff --git a/backend/server.js b/backend/server.js
deleted file mode 100644
index 999c5d5..0000000
--- a/backend/server.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'dotenv/config';
-import express from 'express';
-import cors from 'cors';
-import morgan from 'morgan';
-import { connectDB } from './config/db.js';
-import videoRoutes from './routes/videoRoutes.js';
-import searchRoutes from './routes/searchRoutes.js';
-import commentRoutes from './routes/commentRoutes.js';
-import { categoryRouter, tagRouter } from './routes/metaRoutes.js';
-import statsRoutes from './routes/statsRoutes.js';
-import adRoutes from './routes/adRoutes.js';
-import announcementRoutes from './routes/announcementRoutes.js';
-import settingsRoutes from './routes/settingsRoutes.js';
-
-const app = express();
-const PORT = process.env.PORT || 3000;
-const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/video_site';
-
-app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:5173' }));
-app.use(express.json());
-app.use(morgan('dev'));
-
-app.get('/api/health', (req, res) => {
- res.json({ status: 'ok' });
-});
-
-app.use('/api/videos', videoRoutes);
-app.use('/api/search', searchRoutes);
-app.use('/api/comments', commentRoutes);
-app.use('/api/categories', categoryRouter);
-app.use('/api/tags', tagRouter);
-app.use('/api/stats', statsRoutes);
-app.use('/api/ads', adRoutes);
-app.use('/api/announcement', announcementRoutes);
-app.use('/api/settings', settingsRoutes);
-
-app.use((error, req, res, next) => {
- console.error(error);
- res.status(500).json({ message: 'Internal server error' });
-});
-
-connectDB(MONGODB_URI).then(() => {
- app.listen(PORT, () => {
- console.log(`Backend running on port ${PORT}`);
- });
-});
diff --git a/database.sql b/database.sql
new file mode 100644
index 0000000..2c8da62
--- /dev/null
+++ b/database.sql
@@ -0,0 +1,126 @@
+CREATE DATABASE IF NOT EXISTS video_site CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE video_site;
+
+CREATE TABLE IF NOT EXISTS categories (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL UNIQUE,
+ description TEXT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS tags (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL UNIQUE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS videos (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ description TEXT,
+ thumbnail VARCHAR(500) NOT NULL,
+ video_url VARCHAR(500) NOT NULL,
+ type ENUM('mp4','embed') NOT NULL DEFAULT 'mp4',
+ views INT NOT NULL DEFAULT 0,
+ tags TEXT,
+ category VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS comments (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ video_id INT NOT NULL,
+ content TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT fk_comments_video FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS view_logs (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ video_id INT NOT NULL,
+ ip VARCHAR(100) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT fk_view_logs_video FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS ads (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ image VARCHAR(500) NOT NULL,
+ link VARCHAR(500) NOT NULL,
+ position ENUM('popup','header','middle','footer') NOT NULL,
+ active TINYINT(1) NOT NULL DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS announcements (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ content TEXT NOT NULL,
+ active TINYINT(1) NOT NULL DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS settings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ site_name VARCHAR(255) NOT NULL,
+ logo VARCHAR(500) DEFAULT '',
+ primary_color VARCHAR(20) NOT NULL DEFAULT '#e50914',
+ popup_ads_enabled TINYINT(1) NOT NULL DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS admin (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(255) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+INSERT IGNORE INTO categories (id, name, description) VALUES
+(1, 'Trending', 'Most watched videos'),
+(2, 'Featured', 'Editor picks'),
+(3, 'Action', 'Fast-paced picks'),
+(4, 'Drama', 'Story-rich uploads'),
+(5, 'Music', 'Concert and clip content');
+
+INSERT IGNORE INTO tags (id, name) VALUES
+(1, 'hot'), (2, 'hd'), (3, 'viral'), (4, 'exclusive'), (5, 'night'), (6, 'cinema'), (7, 'trending');
+
+INSERT IGNORE INTO videos (id, title, description, thumbnail, video_url, type, views, tags, category, created_at) VALUES
+(1, 'Night Drop 1', 'Sample featured upload for the PHP version.', 'https://picsum.photos/seed/php-1/640/360', 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4', 'mp4', 1450, 'hot,hd,trending', 'Trending', NOW() - INTERVAL 1 DAY),
+(2, 'Night Drop 2', 'Embed demo content.', 'https://picsum.photos/seed/php-2/640/360', 'https://www.youtube.com/embed/dQw4w9WgXcQ', 'embed', 1280, 'viral,exclusive', 'Featured', NOW() - INTERVAL 2 DAY),
+(3, 'Night Drop 3', 'Action collection sample.', 'https://picsum.photos/seed/php-3/640/360', 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4', 'mp4', 980, 'night,cinema', 'Action', NOW() - INTERVAL 3 DAY),
+(4, 'Night Drop 4', 'Drama sample video.', 'https://picsum.photos/seed/php-4/640/360', 'https://www.youtube.com/embed/dQw4w9WgXcQ', 'embed', 1660, 'hot,viral', 'Drama', NOW() - INTERVAL 4 DAY),
+(5, 'Night Drop 5', 'Music sample video.', 'https://picsum.photos/seed/php-5/640/360', 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4', 'mp4', 2015, 'hd,cinema', 'Music', NOW() - INTERVAL 5 DAY),
+(6, 'Night Drop 6', 'Another featured upload.', 'https://picsum.photos/seed/php-6/640/360', 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4', 'mp4', 1111, 'exclusive,trending', 'Featured', NOW() - INTERVAL 6 DAY),
+(7, 'Night Drop 7', 'Action demo.', 'https://picsum.photos/seed/php-7/640/360', 'https://www.youtube.com/embed/dQw4w9WgXcQ', 'embed', 890, 'night,hot', 'Action', NOW() - INTERVAL 7 DAY),
+(8, 'Night Drop 8', 'Trending clip.', 'https://picsum.photos/seed/php-8/640/360', 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4', 'mp4', 3200, 'viral,hd', 'Trending', NOW() - INTERVAL 8 DAY),
+(9, 'Night Drop 9', 'Drama embed clip.', 'https://picsum.photos/seed/php-9/640/360', 'https://www.youtube.com/embed/dQw4w9WgXcQ', 'embed', 734, 'exclusive,cinema', 'Drama', NOW() - INTERVAL 9 DAY),
+(10, 'Night Drop 10', 'Music upload demo.', 'https://picsum.photos/seed/php-10/640/360', 'https://samplelib.com/lib/preview/mp4/sample-5s.mp4', 'mp4', 1744, 'trending,night', 'Music', NOW() - INTERVAL 10 DAY);
+
+INSERT IGNORE INTO ads (id, image, link, position, active) VALUES
+(1, 'https://picsum.photos/seed/php-header/1200/180', 'https://example.com/header', 'header', 1),
+(2, 'https://picsum.photos/seed/php-middle/1200/180', 'https://example.com/middle', 'middle', 1),
+(3, 'https://picsum.photos/seed/php-footer/1200/180', 'https://example.com/footer', 'footer', 1),
+(4, 'https://picsum.photos/seed/php-popup/1280/720', 'https://example.com/popup', 'popup', 1);
+
+INSERT IGNORE INTO announcements (id, title, content, active) VALUES
+(1, 'Featured Release Tonight', 'New clips and trending videos are now live. Explore the latest uploads and editor picks.', 1);
+
+INSERT IGNORE INTO settings (id, site_name, logo, primary_color, popup_ads_enabled) VALUES
+(1, 'NightFlix PHP', 'https://picsum.photos/seed/php-logo/120/40', '#e50914', 1);
+
+INSERT IGNORE INTO admin (id, username, password) VALUES
+(1, 'admin', '$2y$12$6oyCc.4n3JyP8/fu9NhP3Oitk1ljqOs/qRgFcTZJagOULa8AMe1Nu');
+
+INSERT INTO view_logs (video_id, ip, created_at) VALUES
+(1, '127.0.0.1', NOW() - INTERVAL 1 DAY),
+(1, '127.0.0.1', NOW() - INTERVAL 1 DAY),
+(2, '127.0.0.1', NOW() - INTERVAL 2 DAY),
+(3, '127.0.0.1', NOW() - INTERVAL 2 DAY),
+(3, '127.0.0.1', NOW() - INTERVAL 3 DAY),
+(4, '127.0.0.1', NOW() - INTERVAL 4 DAY),
+(5, '127.0.0.1', NOW() - INTERVAL 15 DAY),
+(6, '127.0.0.1', NOW() - INTERVAL 33 DAY),
+(7, '127.0.0.1', NOW() - INTERVAL 34 DAY),
+(8, '127.0.0.1', NOW() - INTERVAL 60 DAY);
diff --git a/frontend/index.html b/frontend/index.html
deleted file mode 100644
index 6708a38..0000000
--- a/frontend/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- NightFlix
-
-
-
-
-
-
diff --git a/frontend/package.json b/frontend/package.json
deleted file mode 100644
index 650a170..0000000
--- a/frontend/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "video-site-frontend",
- "private": true,
- "version": "1.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "axios": "^1.7.4",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-router-dom": "^6.26.1",
- "recharts": "^2.12.7"
- },
- "devDependencies": {
- "@vitejs/plugin-react": "^4.3.1",
- "vite": "^5.4.2"
- }
-}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
deleted file mode 100644
index a3b79d5..0000000
--- a/frontend/src/App.jsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import { Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
-import { api } from './api/client';
-import { Layout } from './components/Layout';
-import { PopupAd } from './components/PopupAd';
-import { HomePage } from './pages/HomePage';
-import { VideoDetailPage } from './pages/VideoDetailPage';
-import { UploadPage } from './pages/UploadPage';
-import { AdminPage } from './pages/AdminPage';
-
-function VideoDetailRoute({ fetchVideoDetail, videoDetail, loadingVideo }) {
- const { id } = useParams();
- useEffect(() => {
- fetchVideoDetail(id);
- }, [id]);
- return fetchVideoDetail(id)} />;
-}
-
-export default function App() {
- const [videos, setVideos] = useState([]);
- const [categories, setCategories] = useState([]);
- const [tags, setTags] = useState([]);
- const [ads, setAds] = useState([]);
- const [announcement, setAnnouncement] = useState(null);
- const [settings, setSettings] = useState(null);
- const [stats, setStats] = useState(null);
- const [daily, setDaily] = useState([]);
- const [monthly, setMonthly] = useState([]);
- const [loadingVideos, setLoadingVideos] = useState(true);
- const [loadingVideo, setLoadingVideo] = useState(false);
- const [loadingStats, setLoadingStats] = useState(true);
- const [videoDetail, setVideoDetail] = useState(null);
- const [search, setSearch] = useState('');
- const location = useLocation();
- const navigate = useNavigate();
- const query = useMemo(() => new URLSearchParams(location.search), [location.search]);
- const selectedCategory = query.get('category') || '';
- const selectedTag = query.get('tag') || '';
-
- const loadInitial = async () => {
- setLoadingVideos(true);
- const params = {};
- if (selectedCategory) params.category = selectedCategory;
- if (selectedTag) params.tag = selectedTag;
- if (search.trim()) params.q = search.trim();
-
- const [videosRes, categoriesRes, tagsRes, adsRes, announcementRes, settingsRes] = await Promise.all([
- api.get('/videos', { params }),
- api.get('/categories'),
- api.get('/tags'),
- api.get('/ads', { params: { active: true } }),
- api.get('/announcement'),
- api.get('/settings')
- ]);
-
- setVideos(videosRes.data);
- setCategories(categoriesRes.data);
- setTags(tagsRes.data);
- setAds(adsRes.data);
- setAnnouncement(announcementRes.data);
- setSettings(settingsRes.data);
- setLoadingVideos(false);
- };
-
- const loadStats = async () => {
- setLoadingStats(true);
- const [overviewRes, dailyRes, monthlyRes] = await Promise.all([
- api.get('/stats/overview'),
- api.get('/stats/daily'),
- api.get('/stats/monthly')
- ]);
- setStats(overviewRes.data);
- setDaily(dailyRes.data);
- setMonthly(monthlyRes.data);
- setLoadingStats(false);
- };
-
- const fetchVideoDetail = async (id) => {
- setLoadingVideo(true);
- const response = await api.get(`/videos/${id}`);
- setVideoDetail(response.data);
- setLoadingVideo(false);
- };
-
- useEffect(() => {
- loadInitial();
- }, [selectedCategory, selectedTag, search]);
-
- useEffect(() => {
- loadStats();
- }, []);
-
- const setFilter = (key, value) => {
- const next = new URLSearchParams(location.search);
- if (value) next.set(key, value);
- else next.delete(key);
- navigate({ pathname: '/', search: next.toString() });
- };
-
- return (
-
- ad.position === 'popup')} enabled={Boolean(settings?.popupAdsEnabled)} />
-
- setFilter('category', value)} selectedTag={selectedTag} onTagChange={(value) => setFilter('tag', value)} announcement={announcement} ads={ads} />} />
- } />
- } />
- { loadInitial(); loadStats(); }} />} />
-
-
- );
-}
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
deleted file mode 100644
index 463a0fc..0000000
--- a/frontend/src/api/client.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import axios from 'axios';
-
-export const api = axios.create({
- baseURL: 'http://localhost:3000/api'
-});
diff --git a/frontend/src/components/AdBanner.jsx b/frontend/src/components/AdBanner.jsx
deleted file mode 100644
index f9dc6f4..0000000
--- a/frontend/src/components/AdBanner.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export function AdBanner({ ad }) {
- if (!ad?.active) return null;
-
- return (
-
-
-
- );
-}
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
deleted file mode 100644
index 88e4b99..0000000
--- a/frontend/src/components/Layout.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Link, NavLink } from 'react-router-dom';
-
-export function Layout({ settings, search, onSearchChange, children }) {
- return (
-
- );
-}
diff --git a/frontend/src/components/PopupAd.jsx b/frontend/src/components/PopupAd.jsx
deleted file mode 100644
index c37fd2b..0000000
--- a/frontend/src/components/PopupAd.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useEffect, useState } from 'react';
-
-export function PopupAd({ ad, enabled }) {
- const [open, setOpen] = useState(false);
-
- useEffect(() => {
- if (!enabled || !ad?.active) return;
- const alreadySeen = sessionStorage.getItem('popupSeen');
- if (!alreadySeen) {
- setOpen(true);
- sessionStorage.setItem('popupSeen', 'true');
- }
- }, [ad, enabled]);
-
- if (!open || !ad?.active || !enabled) return null;
-
- return (
- setOpen(false)}>
-
event.stopPropagation()}>
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/components/Skeletons.jsx b/frontend/src/components/Skeletons.jsx
deleted file mode 100644
index 141cadd..0000000
--- a/frontend/src/components/Skeletons.jsx
+++ /dev/null
@@ -1,17 +0,0 @@
-export function VideoGridSkeleton() {
- return (
-
- {Array.from({ length: 8 }).map((_, index) => (
-
- ))}
-
- );
-}
-
-export function ChartSkeleton() {
- return ;
-}
diff --git a/frontend/src/components/VideoCard.jsx b/frontend/src/components/VideoCard.jsx
deleted file mode 100644
index c3c82bf..0000000
--- a/frontend/src/components/VideoCard.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Link } from 'react-router-dom';
-
-export function VideoCard({ video }) {
- return (
-
-
-

-
-
-
{video.title}
-
{video.views.toLocaleString()} views
-
{new Date(video.createdAt).toLocaleDateString()}
-
-
- );
-}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
deleted file mode 100644
index 1f82009..0000000
--- a/frontend/src/main.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import { BrowserRouter } from 'react-router-dom';
-import App from './App';
-import './styles/global.css';
-
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-
-
-);
diff --git a/frontend/src/pages/AdminPage.jsx b/frontend/src/pages/AdminPage.jsx
deleted file mode 100644
index ddb2dce..0000000
--- a/frontend/src/pages/AdminPage.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
-import { api } from '../api/client';
-import { ChartSkeleton } from '../components/Skeletons';
-
-function CrudList({ title, items, fields, onCreate, onUpdate, onDelete, initialState }) {
- const [form, setForm] = useState(initialState);
- const [editingId, setEditingId] = useState('');
-
- const submit = async (event) => {
- event.preventDefault();
- if (editingId) {
- await onUpdate(editingId, form);
- } else {
- await onCreate(form);
- }
- setForm(initialState);
- setEditingId('');
- };
-
- return (
-
- {title}
-
-
- {items.map((item) => (
-
-
-
{item.name || item.title || item.siteName || item.position}
- {item.content ?
{item.content}
: null}
-
-
-
-
-
-
- ))}
-
-
- );
-}
-
-export function AdminPage({ stats, daily, monthly, loadingStats, videos, categories, tags, ads, announcement, settings, refreshAll }) {
- const [settingsForm, setSettingsForm] = useState(settings || {});
- useEffect(() => {
- setSettingsForm(settings || {});
- }, [settings]);
- const announcementItems = announcement ? [announcement] : [];
- const overviewCards = useMemo(() => [
- { label: 'Total Videos', value: stats?.totalVideos ?? 0 },
- { label: 'Total Views', value: stats?.totalViews ?? 0 }
- ], [stats]);
-
- const createEntity = (path) => (payload) => api.post(path, payload).then(refreshAll);
- const updateEntity = (path) => (id, payload) => api.put(`${path}/${id}`, payload).then(refreshAll);
- const deleteEntity = (path) => (id) => api.delete(`${path}/${id}`).then(refreshAll);
-
- const saveSettings = async (event) => {
- event.preventDefault();
- await api.post('/settings', settingsForm);
- refreshAll();
- };
-
- return (
-
-
- {overviewCards.map((card) => (
-
- {card.label}
- {card.value}
-
- ))}
-
-
-
-
Daily Views
- {loadingStats ? : (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
Monthly Views
- {loadingStats ? : (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx
deleted file mode 100644
index f387a1b..0000000
--- a/frontend/src/pages/HomePage.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useMemo } from 'react';
-import { VideoCard } from '../components/VideoCard';
-import { VideoGridSkeleton } from '../components/Skeletons';
-import { AdBanner } from '../components/AdBanner';
-
-export function HomePage({ videos, loading, categories, selectedCategory, onCategoryChange, selectedTag, onTagChange, announcement, ads }) {
- const tags = useMemo(() => [...new Set(videos.flatMap((video) => video.tags || []))], [videos]);
- const headerAd = ads.find((ad) => ad.position === 'header' && ad.active);
- const middleAd = ads.find((ad) => ad.position === 'middle' && ad.active);
- const footerAd = ads.find((ad) => ad.position === 'footer' && ad.active);
-
- return (
-
-
- {announcement ? (
-
- {announcement.title}
- {announcement.content}
-
- ) : null}
-
-
- {categories.map((category) => (
-
- ))}
-
-
-
- {tags.map((tag) => (
-
- ))}
-
- {loading ?
:
{videos.map((video) => )}
}
-
-
-
- );
-}
diff --git a/frontend/src/pages/UploadPage.jsx b/frontend/src/pages/UploadPage.jsx
deleted file mode 100644
index 2bd8447..0000000
--- a/frontend/src/pages/UploadPage.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useState } from 'react';
-import { api } from '../api/client';
-
-const initialState = {
- title: '',
- description: '',
- thumbnail: '',
- videoUrl: '',
- type: 'mp4',
- category: '',
- tags: ''
-};
-
-export function UploadPage({ onUploaded }) {
- const [form, setForm] = useState(initialState);
- const [saving, setSaving] = useState(false);
-
- const handleSubmit = async (event) => {
- event.preventDefault();
- setSaving(true);
- await api.post('/videos', form);
- setForm(initialState);
- setSaving(false);
- onUploaded();
- };
-
- return (
-
- );
-}
diff --git a/frontend/src/pages/VideoDetailPage.jsx b/frontend/src/pages/VideoDetailPage.jsx
deleted file mode 100644
index ade9f50..0000000
--- a/frontend/src/pages/VideoDetailPage.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { useState } from 'react';
-import { Link } from 'react-router-dom';
-import { api } from '../api/client';
-import { VideoGridSkeleton } from '../components/Skeletons';
-
-export function VideoDetailPage({ video, loading, onRefresh }) {
- const [comment, setComment] = useState('');
- const [submitting, setSubmitting] = useState(false);
-
- if (loading) return ;
- if (!video) return Video not found.
;
-
- const submitComment = async (event) => {
- event.preventDefault();
- if (!comment.trim()) return;
- setSubmitting(true);
- await api.post('/comments', { videoId: video._id, text: comment });
- setComment('');
- await onRefresh();
- setSubmitting(false);
- };
-
- return (
-
-
- {video.type === 'mp4' ? (
-
- ) : (
-
- )}
- {video.title}
- {video.views.toLocaleString()} views • {new Date(video.createdAt).toLocaleDateString()}
- {video.description}
-
- {video.tags.map((tag) => (
- #{tag}
- ))}
-
-
-
- {video.comments.map((item) => (
-
- {item.text}
- {new Date(item.createdAt).toLocaleString()}
-
- ))}
-
-
-
-
- );
-}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
deleted file mode 100644
index 88ed866..0000000
--- a/frontend/src/styles/global.css
+++ /dev/null
@@ -1,63 +0,0 @@
-:root {
- color-scheme: dark;
- font-family: Inter, system-ui, sans-serif;
- background: #050505;
- color: #f5f5f5;
-}
-* { box-sizing: border-box; }
-body { margin: 0; background: #050505; color: #f5f5f5; }
-a { color: inherit; text-decoration: none; }
-button, input, textarea, select { font: inherit; }
-.app-shell { min-height: 100vh; background: linear-gradient(180deg, #080808 0%, #111 100%); }
-.site-header { position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: 1rem; padding: 1rem 1.5rem; background: rgba(0,0,0,.92); border-bottom: 1px solid #1f1f1f; }
-.brand { display: flex; align-items: center; gap: .75rem; font-size: 1.2rem; font-weight: 700; min-width: 170px; }
-.brand-logo { width: 42px; height: 42px; border-radius: 10px; object-fit: cover; }
-.search-input { flex: 1; background: #141414; border: 1px solid #2a2a2a; border-radius: 999px; color: #fff; padding: .8rem 1rem; }
-.nav-links { display: flex; gap: 1rem; }
-.nav-links a.active, .nav-links a:hover, .filter-bar button.active, .tag-bar button.active, .tag-link:hover { color: white; background: var(--primary); }
-.content { max-width: 1400px; margin: 0 auto; padding: 1.5rem; }
-.page-stack { display: flex; flex-direction: column; gap: 1.25rem; }
-.announcement, .chart-card, .admin-section, .player-panel, .related-panel, .overview-card { background: #111; border: 1px solid #1e1e1e; border-radius: 18px; padding: 1rem; box-shadow: 0 20px 40px rgba(0,0,0,.2); }
-.filter-bar, .tag-bar { display: flex; flex-wrap: wrap; gap: .75rem; }
-.filter-bar button, .tag-bar button, .tag-link, .admin-actions button, .comment-form button, .admin-form button { background: #181818; color: #d5d5d5; border: 1px solid #2b2b2b; border-radius: 999px; padding: .65rem 1rem; cursor: pointer; transition: .2s ease; }
-.video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
-.video-card { display: flex; flex-direction: column; background: #101010; border: 1px solid #1f1f1f; border-radius: 20px; overflow: hidden; transition: transform .25s ease, box-shadow .25s ease; }
-.video-card:hover { transform: translateY(-6px) scale(1.01); box-shadow: 0 18px 32px rgba(229,9,20,.15); }
-.thumbnail-wrap { aspect-ratio: 16 / 9; overflow: hidden; }
-.thumbnail { width: 100%; height: 100%; object-fit: cover; transition: transform .3s ease; }
-.video-card:hover .thumbnail { transform: scale(1.08); }
-.video-card-body { padding: .9rem; display: grid; gap: .35rem; }
-.video-card-body h3 { margin: 0; font-size: 1rem; }
-.video-card-body p, .video-card-body span, .video-meta, .comment-item span { color: #9f9f9f; margin: 0; }
-.detail-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr); gap: 1rem; }
-.video-player { width: 100%; max-height: 70vh; border: none; border-radius: 16px; background: #000; }
-.iframe-player { aspect-ratio: 16/9; min-height: 420px; }
-.related-card { display: grid; grid-template-columns: 120px 1fr; gap: .75rem; padding: .6rem 0; border-bottom: 1px solid #1f1f1f; }
-.related-card img { width: 120px; height: 68px; object-fit: cover; border-radius: 10px; }
-.comment-form, .admin-form { display: grid; gap: .85rem; }
-.comment-form textarea, .admin-form input, .admin-form textarea, .admin-form select { width: 100%; background: #181818; border: 1px solid #2a2a2a; border-radius: 14px; color: #fff; padding: .85rem 1rem; }
-.comment-list, .admin-list { display: grid; gap: .75rem; }
-.comment-item, .admin-item { background: #161616; border: 1px solid #242424; border-radius: 14px; padding: .85rem 1rem; }
-.admin-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
-.admin-actions { display: flex; gap: .5rem; }
-.admin-actions .danger { background: rgba(229,9,20,.15); color: #ff8c8c; }
-.overview-grid, .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }
-.overview-card strong { font-size: 2rem; margin-top: .4rem; display: block; }
-.ad-banner img { width: 100%; border-radius: 18px; display: block; }
-.popup-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.8); display: grid; place-items: center; z-index: 100; padding: 1rem; }
-.popup-card { position: relative; max-width: 900px; width: 100%; }
-.popup-card img { width: 100%; border-radius: 24px; display: block; }
-.popup-close { position: absolute; top: .75rem; right: .75rem; width: 42px; height: 42px; border-radius: 999px; border: none; background: rgba(0,0,0,.75); color: #fff; font-size: 1.5rem; cursor: pointer; }
-.skeleton { background: linear-gradient(90deg, #171717 25%, #222 50%, #171717 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
-.skeleton-card { display: grid; gap: .8rem; }
-.thumbnail-skeleton { aspect-ratio: 16/9; border-radius: 20px; }
-.line { height: 18px; border-radius: 999px; }
-.line.short { width: 60%; }
-.chart-skeleton { height: 280px; border-radius: 18px; }
-.checkbox-row { display: flex; align-items: center; justify-content: space-between; }
-@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
-@media (max-width: 900px) {
- .site-header { flex-wrap: wrap; }
- .detail-layout { grid-template-columns: 1fr; }
- .iframe-player { min-height: 240px; }
-}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
deleted file mode 100644
index efe6335..0000000
--- a/frontend/vite.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { defineConfig } from 'vite';
-import react from '@vitejs/plugin-react';
-
-export default defineConfig({
- plugins: [react()],
- server: {
- port: 5173
- }
-});
diff --git a/includes/config.php b/includes/config.php
new file mode 100644
index 0000000..a871c55
--- /dev/null
+++ b/includes/config.php
@@ -0,0 +1,23 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+];
+
+try {
+ $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
+} catch (PDOException $exception) {
+ die('Database connection failed: ' . htmlspecialchars($exception->getMessage()));
+}
diff --git a/includes/footer.php b/includes/footer.php
new file mode 100644
index 0000000..2685418
--- /dev/null
+++ b/includes/footer.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+