Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/bao-03022026-Admin-Management-Service

This commit is contained in:
nguyenvanbao
2026-02-04 09:18:42 +07:00
25 changed files with 4638 additions and 925 deletions

View File

@@ -0,0 +1,377 @@
const AppointmentSubmission = require("../models/appointmentSubmission");
const Appointment = require("../models/appointment");
// ==================== CMS ADMIN FUNCTIONS ====================
// Render admin page for appointment management
exports.index = async (req, res) => {
try {
let appointment = await Appointment.findOne({ name: "default" });
// If no data in DB, try to load from JSON file
if (!appointment) {
const fs = require("fs");
const path = require("path");
const jsonPath = path.join(__dirname, "../data/appointment.json");
if (fs.existsSync(jsonPath)) {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
appointment = await Appointment.migrateFromJson(jsonData);
} else {
// Create default appointment
appointment = await Appointment.create({
name: "default",
hero: {
title: "Make Appointment",
backgroundImage: "",
subtitle: "",
heading: "",
description: "",
},
visaOptions: [],
form: {
heading: "Request Appointment",
fields: [],
submitButton: {
text: "Request Appointment",
icon: "fa-solid fa-arrow-right",
buttonClass: "theme-btn",
},
},
});
}
}
const { startDate, endDate } = req.query;
const query = {};
if (startDate || endDate) {
query.createdAt = {};
if (startDate) {
query.createdAt.$gte = new Date(startDate);
}
if (endDate) {
// Set end date to end of day
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
query.createdAt.$lte = end;
}
}
const submissions = await AppointmentSubmission.find(query).sort({ createdAt: -1 }).limit(50);
res.render("admin/appointment/index", {
layout: "layouts/main",
title: "Appointment Management",
data: appointment,
submissions,
startDate,
endDate,
user: req.session.user,
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
});
} catch (err) {
console.error("Error loading appointment admin page:", err);
req.flash("error", "Error loading appointment data");
res.redirect("/admin/dashboard");
}
};
// Update appointment data
exports.update = async (req, res) => {
try {
const { hero, visaOptions, form } = req.body;
// Parse JSON strings if needed
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
const visaOptionsData = typeof visaOptions === "string" ? JSON.parse(visaOptions) : visaOptions;
const formData = typeof form === "string" ? JSON.parse(form) : form;
let appointment = await Appointment.findOne({ name: "default" });
if (appointment) {
appointment.hero = heroData;
appointment.visaOptions = visaOptionsData;
appointment.form = formData;
await appointment.save();
} else {
appointment = await Appointment.create({
name: "default",
hero: heroData,
visaOptions: visaOptionsData,
form: formData,
});
}
req.flash("success", "Appointment data updated successfully");
res.redirect("/admin/appointment");
} catch (err) {
console.error("Error updating appointment:", err);
req.flash("error", "Error updating appointment data");
res.redirect("/admin/appointment");
}
};
// API to get appointment data
exports.getAppointmentData = async (req, res) => {
try {
let appointment = await Appointment.findOne({ name: "default" });
if (!appointment) {
const fs = require("fs");
const path = require("path");
const jsonPath = path.join(__dirname, "../data/appointment.json");
if (fs.existsSync(jsonPath)) {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
appointment = await Appointment.migrateFromJson(jsonData);
}
}
res.json({
success: true,
data: appointment,
});
} catch (err) {
console.error("Error getting appointment data:", err);
res.status(500).json({
success: false,
error: "Error loading appointment data",
});
}
};
// Public API to get appointment page data (for frontend)
exports.api = async (req, res) => {
try {
let appointment = await Appointment.findOne({ name: "default" });
if (!appointment) {
const fs = require("fs");
const path = require("path");
const jsonPath = path.join(__dirname, "../data/appointment.json");
if (fs.existsSync(jsonPath)) {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
appointment = await Appointment.migrateFromJson(jsonData);
}
}
if (!appointment) {
return res.status(404).json({
success: false,
error: "Appointment data not found",
});
}
res.json({
success: true,
data: {
hero: appointment.hero,
visaOptions: appointment.visaOptions,
form: appointment.form,
},
});
} catch (err) {
console.error("Error getting appointment API data:", err);
res.status(500).json({
success: false,
error: "Error loading appointment data",
});
}
};
// ==================== APPOINTMENT SUBMISSIONS API ====================
// API để submit appointment form (từ frontend)
exports.submitAppointment = async (req, res) => {
try {
const { name, email, phone, address, appointmentDate, message, visaTypes } = req.body;
// Validation
if (!name || !email) {
return res.status(400).json({
success: false,
error: "Name and email are required",
});
}
// Create new submission
const submission = new AppointmentSubmission({
name: name.trim(),
email: email.trim().toLowerCase(),
phone: phone?.trim() || "",
address: address?.trim() || "",
appointmentDate: appointmentDate?.trim() || "",
message: message?.trim() || "",
visaTypes: Array.isArray(visaTypes) ? visaTypes : [],
ipAddress: req.ip || req.connection?.remoteAddress || "",
userAgent: req.get("User-Agent") || "",
});
await submission.save();
res.status(201).json({
success: true,
message: "Thank you! Your appointment request has been submitted. We will contact you soon.",
data: {
id: submission._id,
name: submission.name,
email: submission.email,
appointmentDate: submission.appointmentDate,
},
});
} catch (err) {
console.error("Error submitting appointment:", err);
// Handle validation errors
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map((e) => e.message);
return res.status(400).json({
success: false,
error: errors.join(", "),
});
}
res.status(500).json({
success: false,
error: "Error submitting appointment. Please try again later.",
});
}
};
// API để lấy danh sách appointments (cho admin)
exports.getAppointments = async (req, res) => {
try {
const { status, page = 1, limit = 20 } = req.query;
const query = {};
if (status && ["pending", "confirmed", "completed", "cancelled"].includes(status)) {
query.status = status;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [appointments, total] = await Promise.all([
AppointmentSubmission.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
AppointmentSubmission.countDocuments(query),
]);
res.json({
success: true,
data: appointments,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages: Math.ceil(total / parseInt(limit)),
},
});
} catch (err) {
console.error("Error getting appointments:", err);
res.status(500).json({
success: false,
error: "Error loading appointments",
});
}
};
// API để cập nhật status của appointment
exports.updateAppointmentStatus = async (req, res) => {
try {
const { id } = req.params;
const { status, notes } = req.body;
const validStatuses = ["pending", "confirmed", "completed", "cancelled"];
if (!validStatuses.includes(status)) {
return res.status(400).json({
success: false,
error: "Invalid status",
});
}
const updateData = { status };
if (notes !== undefined) updateData.notes = notes;
if (status === "confirmed") updateData.confirmedAt = new Date();
if (status === "completed") updateData.completedAt = new Date();
const appointment = await AppointmentSubmission.findByIdAndUpdate(
id,
updateData,
{ new: true }
);
if (!appointment) {
return res.status(404).json({
success: false,
error: "Appointment not found",
});
}
res.json({
success: true,
data: appointment,
});
} catch (err) {
console.error("Error updating appointment:", err);
res.status(500).json({
success: false,
error: "Error updating appointment",
});
}
};
// API để lấy chi tiết một appointment
exports.getAppointmentById = async (req, res) => {
try {
const { id } = req.params;
const appointment = await AppointmentSubmission.findById(id);
if (!appointment) {
return res.status(404).json({
success: false,
error: "Appointment not found",
});
}
res.json({
success: true,
data: appointment,
});
} catch (err) {
console.error("Error getting appointment:", err);
res.status(500).json({
success: false,
error: "Error loading appointment",
});
}
};
// API để xóa appointment
exports.deleteAppointment = async (req, res) => {
try {
const { id } = req.params;
const appointment = await AppointmentSubmission.findByIdAndDelete(id);
if (!appointment) {
return res.status(404).json({
success: false,
error: "Appointment not found",
});
}
res.json({
success: true,
message: "Appointment deleted successfully",
});
} catch (err) {
console.error("Error deleting appointment:", err);
res.status(500).json({
success: false,
error: "Error deleting appointment",
});
}
};

View File

@@ -1,5 +1,6 @@
const { addBaseUrlToImages } = require("../utils/imageHelper"); const { addBaseUrlToImages } = require("../utils/imageHelper");
const Contact = require("../models/contact"); const Contact = require("../models/contact");
const ContactSubmission = require("../models/contactSubmission");
// Get contact data from MongoDB // Get contact data from MongoDB
const getContactData = async () => { const getContactData = async () => {
@@ -60,6 +61,7 @@ exports.index = async (req, res) => {
zoom: 15, zoom: 15,
location: "", location: "",
markerTitle: "", markerTitle: "",
embedUrl: "",
tileLayer: { tileLayer: {
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: "", attribution: "",
@@ -70,16 +72,38 @@ exports.index = async (req, res) => {
form: { form: {
sectionLabel: "", sectionLabel: "",
heading: "", heading: "",
description: "",
fields: [], fields: [],
submitButton: { text: "Send Message" }, submitButton: { text: "Send Message", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" },
}, },
}; };
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const { startDate, endDate } = req.query;
const query = {};
if (startDate || endDate) {
query.createdAt = {};
if (startDate) {
query.createdAt.$gte = new Date(startDate);
}
if (endDate) {
// Set end date to end of day
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
query.createdAt.$lte = end;
}
}
const submissions = await ContactSubmission.find(query).sort({ createdAt: -1 }).limit(50);
const frontendUrl = process.env.FRONTEND_URL;
res.render("admin/contact/index", { res.render("admin/contact/index", {
title: "Contact Management", title: "Contact Management",
layout: "layouts/main", layout: "layouts/main",
data, data,
submissions,
startDate,
endDate,
frontendUrl, frontendUrl,
currentPath: req.path, currentPath: req.path,
user: req.session.user, user: req.session.user,
@@ -140,6 +164,7 @@ exports.update = async (req, res) => {
zoom: 15, zoom: 15,
location: "", location: "",
markerTitle: "", markerTitle: "",
embedUrl: "",
tileLayer: { tileLayer: {
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: "", attribution: "",
@@ -150,8 +175,9 @@ exports.update = async (req, res) => {
form: formData || { form: formData || {
sectionLabel: "", sectionLabel: "",
heading: "", heading: "",
description: "",
fields: [], fields: [],
submitButton: { text: "Send Message" }, submitButton: { text: "Send Message", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" },
}, },
}); });
} else { } else {
@@ -179,3 +205,141 @@ exports.update = async (req, res) => {
res.redirect("/admin/contact"); res.redirect("/admin/contact");
} }
}; };
// API để submit contact form (từ frontend)
exports.submitForm = async (req, res) => {
try {
const { name, email, phone, address, date, message } = req.body;
// Validation
if (!name || !email) {
return res.status(400).json({
success: false,
error: "Name and email are required",
});
}
// Create new submission
const submission = new ContactSubmission({
name: name.trim(),
email: email.trim().toLowerCase(),
phone: phone?.trim() || "",
address: address?.trim() || "",
date: date?.trim() || "",
message: message?.trim() || "",
ipAddress: req.ip || req.connection?.remoteAddress || "",
userAgent: req.get("User-Agent") || "",
});
await submission.save();
res.status(201).json({
success: true,
message: "Thank you for contacting us! We will get back to you soon.",
data: {
id: submission._id,
name: submission.name,
email: submission.email,
},
});
} catch (err) {
console.error("Error submitting contact form:", err);
// Handle validation errors
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map((e) => e.message);
return res.status(400).json({
success: false,
error: errors.join(", "),
});
}
res.status(500).json({
success: false,
error: "Error submitting form. Please try again later.",
});
}
};
// API để lấy danh sách submissions (cho admin)
exports.getSubmissions = async (req, res) => {
try {
const { status, page = 1, limit = 20 } = req.query;
const query = {};
if (status && ["pending", "read", "replied", "archived"].includes(status)) {
query.status = status;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [submissions, total] = await Promise.all([
ContactSubmission.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
ContactSubmission.countDocuments(query),
]);
res.json({
success: true,
data: submissions,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages: Math.ceil(total / parseInt(limit)),
},
});
} catch (err) {
console.error("Error getting submissions:", err);
res.status(500).json({
success: false,
error: "Error loading submissions",
});
}
};
// API để cập nhật status của submission
exports.updateSubmissionStatus = async (req, res) => {
try {
const { id } = req.params;
const { status, notes } = req.body;
const validStatuses = ["pending", "read", "replied", "archived"];
if (!validStatuses.includes(status)) {
return res.status(400).json({
success: false,
error: "Invalid status",
});
}
const updateData = { status };
if (notes !== undefined) updateData.notes = notes;
if (status === "replied") updateData.repliedAt = new Date();
const submission = await ContactSubmission.findByIdAndUpdate(
id,
updateData,
{ new: true }
);
if (!submission) {
return res.status(404).json({
success: false,
error: "Submission not found",
});
}
res.json({
success: true,
data: submission,
});
} catch (err) {
console.error("Error updating submission:", err);
res.status(500).json({
success: false,
error: "Error updating submission",
});
}
};

View File

@@ -0,0 +1,193 @@
const Pricing = require("../models/pricing");
// ==================== CMS ADMIN FUNCTIONS ====================
// Render admin page for pricing management
exports.index = async (req, res) => {
try {
let pricing = await Pricing.findOne({ name: "default" });
// If no data in DB, try to load from JSON file
if (!pricing) {
const fs = require("fs");
const path = require("path");
const jsonPath = path.join(__dirname, "../data/pricing.json");
if (fs.existsSync(jsonPath)) {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
pricing = await Pricing.migrateFromJson(jsonData);
} else {
// Create default pricing
pricing = await Pricing.create({
name: "default",
hero: {
title: "Pricing Plan",
backgroundImage: "/assets/img/inner-page/breadcrumb.jpg",
shapeImage: "/assets/img/inner-page/shape.png",
breadcrumb: [
{ text: "Home", link: "/" },
{ text: "Pricing Plan", link: "" },
],
},
pricingSection: {
subtitle: "pricing plan",
heading: "Flexible Plans to Suit Every Traveler",
description: "Choose the plan that fits your visa needs and enjoy expert guidance every step of the way.",
},
plans: {
monthly: [],
yearly: [],
},
testimonials: {
subtitle: "What Our Clients Say",
heading: "Immigration Success Stories",
buttonText: "View All Review",
buttonLink: "/contact",
buttonIcon: "fa-solid fa-arrow-right",
image: "",
items: [],
},
});
}
}
res.render("admin/pricing/index", {
layout: "layouts/main",
title: "Pricing Management",
data: pricing,
user: req.session.user,
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
});
} catch (err) {
console.error("Error loading pricing admin page:", err);
req.flash("error", "Error loading pricing data");
res.redirect("/admin/dashboard");
}
};
// Update pricing data
exports.update = async (req, res) => {
try {
const { hero, pricingSection, plans, testimonials } = req.body;
// Parse JSON strings if needed
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
const pricingSectionData = typeof pricingSection === "string" ? JSON.parse(pricingSection) : pricingSection;
const plansData = typeof plans === "string" ? JSON.parse(plans) : plans;
const testimonialsData = typeof testimonials === "string" ? JSON.parse(testimonials) : testimonials;
let pricing = await Pricing.findOne({ name: "default" });
if (pricing) {
pricing.hero = heroData;
pricing.pricingSection = pricingSectionData;
pricing.plans = plansData;
pricing.testimonials = testimonialsData;
await pricing.save();
} else {
pricing = await Pricing.create({
name: "default",
hero: heroData,
pricingSection: pricingSectionData,
plans: plansData,
testimonials: testimonialsData,
});
}
req.flash("success", "Pricing data updated successfully");
res.redirect("/admin/pricing");
} catch (err) {
console.error("Error updating pricing:", err);
req.flash("error", "Error updating pricing data");
res.redirect("/admin/pricing");
}
};
// API to get pricing data (admin)
exports.getPricingData = async (req, res) => {
try {
let pricing = await Pricing.findOne({ name: "default" });
if (!pricing) {
const fs = require("fs");
const path = require("path");
const jsonPath = path.join(__dirname, "../data/pricing.json");
if (fs.existsSync(jsonPath)) {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
pricing = await Pricing.migrateFromJson(jsonData);
}
}
res.json({
success: true,
data: pricing,
});
} catch (err) {
console.error("Error getting pricing data:", err);
res.status(500).json({
success: false,
error: "Error loading pricing data",
});
}
};
// Public API to get pricing page data (for frontend)
exports.api = async (req, res) => {
try {
let pricing = await Pricing.findOne({ name: "default" });
if (!pricing) {
const fs = require("fs");
const path = require("path");
const jsonPath = path.join(__dirname, "../data/pricing.json");
if (fs.existsSync(jsonPath)) {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
pricing = await Pricing.migrateFromJson(jsonData);
}
}
if (!pricing) {
return res.status(404).json({
success: false,
error: "Pricing data not found",
});
}
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
const getFullUrl = (path) => {
if (!path || path.startsWith("http")) return path;
return `${backendUrl}${path.startsWith("/") ? "" : "/"}${path}`;
};
// Convert to plain object to modify properties safely
const pricingData = pricing.toObject ? pricing.toObject() : pricing;
if (pricingData.hero) {
pricingData.hero.backgroundImage = getFullUrl(pricingData.hero.backgroundImage);
pricingData.hero.shapeImage = getFullUrl(pricingData.hero.shapeImage);
}
if (pricingData.testimonials) {
pricingData.testimonials.image = getFullUrl(pricingData.testimonials.image);
}
res.json({
success: true,
data: {
hero: pricingData.hero,
pricingSection: pricingData.pricingSection,
plans: pricingData.plans,
testimonials: pricingData.testimonials,
},
});
} catch (err) {
console.error("Error getting pricing API data:", err);
res.status(500).json({
success: false,
error: "Error loading pricing data",
});
}
};

77
data/appointment.json Normal file
View File

@@ -0,0 +1,77 @@
{
"hero": {
"title": "Make Appointment",
"backgroundImage": "/assets/img/inner-page/breadcrumb.jpg",
"subtitle": "About Our Consultancy",
"heading": "Want to meet us for your need?",
"description": "24/7 customer support is always ready to answer all your questions"
},
"visaOptions": [
"Canada Immigration",
"Tourist Visa",
"Medical Visa",
"Coaching",
"Student Visa",
"Spouse Visa",
"Job Opportunity",
"Exam"
],
"form": {
"heading": "Request Appointment",
"fields": [
{
"name": "name",
"label": "Your Name",
"type": "text",
"placeholder": "Your name",
"required": true,
"colClass": "col-lg-4"
},
{
"name": "email",
"label": "Your Email",
"type": "email",
"placeholder": "Your email",
"required": true,
"colClass": "col-lg-4"
},
{
"name": "phone",
"label": "Your Phone",
"type": "tel",
"placeholder": "Phone Number",
"required": false,
"colClass": "col-lg-4"
},
{
"name": "address",
"label": "Your Address",
"type": "text",
"placeholder": "Your address",
"required": false,
"colClass": "col-lg-6"
},
{
"name": "appointmentDate",
"label": "Appointment Date",
"type": "date",
"placeholder": "",
"required": false,
"colClass": "col-lg-6"
},
{
"name": "message",
"label": "Your Message",
"type": "textarea",
"placeholder": "Type your message",
"required": false,
"colClass": "col-lg-12"
}
],
"submitButton": {
"text": "Request Appointment",
"icon": "fa-solid fa-arrow-right",
"buttonClass": "theme-btn"
}
}
}

View File

@@ -1,95 +0,0 @@
{
"hero": {
"title": "Contact Us",
"backgroundImage": "/uploads/banner/b10.jpg",
"overlayColor": "rgba(0, 0, 0, 0)",
"sectionClass": "uk-section-secondary uk-section-overlap uk-preserve-color uk-light",
"titleClass": "uk-heading-large uk-text-center !text-[5vw]",
"enableScrollspy": true,
"backgroundPosition": "top-center"
},
"contactCards": [
{
"type": "phone",
"title": "Phone Number",
"content": ["+123456789"],
"iconType": "fas fa-phone",
"iconSource": "fontawesome"
},
{
"type": "email",
"title": "Email Address",
"content": ["office@ggcamp.org"],
"iconType": "fas fa-envelope",
"iconSource": "fontawesome"
},
{
"type": "location",
"title": "Our Location",
"content": ["Poblacion, Madridejos 22, Cebu City, Philippines"],
"iconType": "fas fa-map-marker-alt",
"iconSource": "fontawesome"
},
{
"type": "hours",
"title": "Working hours",
"content": ["Monday to Saturday: 07pm - 05am", "Sunday: Closed"],
"iconType": "fas fa-clock",
"iconSource": "fontawesome"
}
],
"map": {
"coordinates": {
"lat": 10.3157,
"lng": 123.8854
},
"zoom": 15,
"location": "Poblacion, Madridejos 22, Cebu City, Philippines",
"markerTitle": "Our Office",
"tileLayer": {
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors",
"maxZoom": 18,
"minZoom": 0
}
},
"form": {
"sectionLabel": "Contact Us",
"heading": "Let's plan your dream adventure - contact our team today",
"fields": [
{
"name": "firstName",
"type": "text",
"placeholder": "First name",
"required": true
},
{
"name": "lastName",
"type": "text",
"placeholder": "Last name",
"required": true
},
{
"name": "phone",
"type": "tel",
"placeholder": "Phone Number",
"required": true
},
{
"name": "email",
"type": "email",
"placeholder": "Email Address",
"required": true
},
{
"name": "message",
"type": "textarea",
"placeholder": "Send Message",
"required": true
}
],
"submitButton": {
"text": "Send Message"
}
}
}

119
data/contact.json Normal file
View File

@@ -0,0 +1,119 @@
{
"hero": {
"title": "CONTACT US",
"backgroundImage": "/assets/img/inner-page/breadcrumb.jpg",
"overlayColor": "rgba(0, 0, 0, 0)",
"sectionClass": "breadcrumb-wrapper fix bg-cover",
"titleClass": "breadcrumb-title",
"enableScrollspy": false,
"backgroundPosition": "center"
},
"contactCards": [
{
"type": "location",
"title": "Location",
"content": [
"43 Sardinella, 3nd Land Walk,",
"Orchard view, London, UK"
],
"iconType": "fa-solid fa-location-dot",
"iconSource": "fontawesome"
},
{
"type": "email",
"title": "Email Address",
"content": [
"supportinfo@gmail.com",
"arluxhotelinfo.com"
],
"iconType": "fa-solid fa-envelope",
"iconSource": "fontawesome"
},
{
"type": "phone",
"title": "Phone Number",
"content": [
"+880 123 427 00",
"+000 938 809 12"
],
"iconType": "fa-solid fa-phone",
"iconSource": "fontawesome"
}
],
"map": {
"coordinates": {
"lat": -37.81450084255415,
"lng": 144.9618311901502
},
"zoom": 15,
"location": "Envato, Melbourne, Australia",
"markerTitle": "Our Office",
"embedUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d6678.7619084840835!2d144.9618311901502!3d-37.81450084255415!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x6ad642b4758afc1d%3A0x3119cc820fdfc62e!2sEnvato!5e0!3m2!1sen!2sbd!4v1641984054261!5m2!1sen!2sbd",
"tileLayer": {
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "",
"maxZoom": 18,
"minZoom": 0
}
},
"form": {
"sectionLabel": "",
"heading": "Send Us Message",
"description": "Have questions about visas or immigration? Send us a message today and our expert team will respond quickly.",
"fields": [
{
"name": "name",
"label": "Your Name",
"type": "text",
"placeholder": "Your name",
"required": true,
"colClass": "col-lg-4"
},
{
"name": "email",
"label": "Your Email",
"type": "email",
"placeholder": "Your email",
"required": true,
"colClass": "col-lg-4"
},
{
"name": "phone",
"label": "Your Phone",
"type": "tel",
"placeholder": "Phone Number",
"required": true,
"colClass": "col-lg-4"
},
{
"name": "address",
"label": "Your Address",
"type": "text",
"placeholder": "Address Now",
"required": false,
"colClass": "col-lg-6"
},
{
"name": "date",
"label": "Your Date",
"type": "date",
"placeholder": "Date",
"required": false,
"colClass": "col-lg-6"
},
{
"name": "message",
"label": "Your Message",
"type": "textarea",
"placeholder": "Type your message",
"required": false,
"colClass": "col-lg-12"
}
],
"submitButton": {
"text": "SEND MESSAGE",
"icon": "fa-solid fa-arrow-right",
"buttonClass": "theme-btn style-2"
}
}
}

118
data/pricing.json Normal file
View File

@@ -0,0 +1,118 @@
{
"hero": {
"title": "Pricing Plan",
"backgroundImage": "/assets/img/inner-page/breadcrumb.jpg",
"shapeImage": "/assets/img/inner-page/shape.png",
"breadcrumb": [
{
"text": "Home",
"link": "/"
},
{
"text": "Pricing Plan",
"link": ""
}
]
},
"pricingSection": {
"subtitle": "pricing plan",
"heading": "Flexible Plans to Suit Every Traveler",
"description": "Choose the plan that fits your visa needs and enjoy expert guidance every step of the way."
},
"plans": {
"monthly": [
{
"name": "Basic Plan",
"price": "32",
"period": "mo",
"currency": "$",
"buttonText": "Get Started Today",
"buttonLink": "/pricing",
"buttonIcon": "fa-solid fa-arrow-right",
"style": "default",
"features": [
"Everything in Basic Plan",
"Visa Interview Preparation",
"Priority Processing Support",
"Phone & Email Assistance",
"Step-by-Step Application Support"
]
},
{
"name": "Premium Plan",
"price": "32",
"period": "mo",
"currency": "$",
"buttonText": "Get Started Today",
"buttonLink": "/pricing",
"buttonIcon": "fa-solid fa-arrow-right",
"style": "style-2",
"features": [
"Everything in Basic Plan",
"Visa Interview Preparation",
"Priority Processing Support",
"Phone & Email Assistance",
"Step-by-Step Application Support"
]
}
],
"yearly": [
{
"name": "Basic Plan",
"price": "32",
"period": "mo",
"currency": "$",
"buttonText": "Get Started Today",
"buttonLink": "/pricing",
"buttonIcon": "fa-solid fa-arrow-right",
"style": "default",
"features": [
"Everything in Basic Plan",
"Visa Interview Preparation",
"Priority Processing Support",
"Phone & Email Assistance",
"Step-by-Step Application Support"
]
},
{
"name": "Premium Plan",
"price": "32",
"period": "mo",
"currency": "$",
"buttonText": "Get Started Today",
"buttonLink": "/pricing",
"buttonIcon": "fa-solid fa-arrow-right",
"style": "style-2",
"features": [
"Everything in Basic Plan",
"Visa Interview Preparation",
"Priority Processing Support",
"Phone & Email Assistance",
"Step-by-Step Application Support"
]
}
]
},
"testimonials": {
"subtitle": "What Our Clients Say",
"heading": "Immigration Success Stories",
"buttonText": "View All Review",
"buttonLink": "/contact",
"buttonIcon": "fa-solid fa-arrow-right",
"image": "/assets/img/home-3/test-thumb.jpg",
"items": [
{
"name": "Mohammed Ali",
"role": "Family Visa",
"rating": 5,
"content": "The team provided exceptional guidance throughout my immigration process. Their expertise, personalized support, and attention to detail ensured a smooth, stress-free experience and successful visa approval."
},
{
"name": "Mohammed Ali",
"role": "Family Visa",
"rating": 5,
"content": "The team provided exceptional guidance throughout my immigration process. Their expertise, personalized support, and attention to detail ensured a smooth, stress-free experience and successful visa approval."
}
]
}
}

206
models/appointment.js Normal file
View File

@@ -0,0 +1,206 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Appointment) {
delete mongoose.models.Appointment;
}
if (mongoose.connection.models.Appointment) {
delete mongoose.connection.models.Appointment;
}
// Schema cho hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Make Appointment",
},
backgroundImage: {
type: String,
trim: true,
default: "",
},
subtitle: {
type: String,
trim: true,
default: "",
},
heading: {
type: String,
trim: true,
default: "",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema cho form field
const formFieldSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
label: {
type: String,
trim: true,
default: "",
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "date", "select"],
},
placeholder: {
type: String,
trim: true,
default: "",
},
required: {
type: Boolean,
default: false,
},
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
},
{ _id: false }
);
// Schema cho submit button
const submitButtonSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
default: "Request Appointment",
},
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn",
},
},
{ _id: false }
);
// Schema cho form
const formSchema = new mongoose.Schema(
{
heading: {
type: String,
trim: true,
default: "Request Appointment",
},
fields: {
type: [formFieldSchema],
default: [],
},
submitButton: {
type: submitButtonSchema,
default: () => ({}),
},
},
{ _id: false }
);
// Main Appointment Schema
const appointmentSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
visaOptions: {
type: [String],
default: [],
},
form: {
type: formSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
appointmentSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default appointment exists
const existingAppointment = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Make Appointment",
backgroundImage: jsonData.hero?.backgroundImage || "",
subtitle: jsonData.hero?.subtitle || "",
heading: jsonData.hero?.heading || "",
description: jsonData.hero?.description || "",
},
visaOptions: Array.isArray(jsonData.visaOptions) ? jsonData.visaOptions : [],
form: {
heading: jsonData.form?.heading || "Request Appointment",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
label: field.label || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
colClass: field.colClass || "col-lg-12",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Request Appointment",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn",
},
},
};
if (existingAppointment) {
// Update existing appointment
existingAppointment.hero = processedData.hero;
existingAppointment.visaOptions = processedData.visaOptions;
existingAppointment.form = processedData.form;
await existingAppointment.save();
console.log("Appointment data updated successfully");
return existingAppointment;
} else {
// Create new appointment
const newAppointment = await this.create({
name: "default",
...processedData,
});
console.log("Appointment data imported successfully");
return newAppointment;
}
} catch (error) {
console.error("Error migrating appointment data:", error);
throw error;
}
};
module.exports = mongoose.model("Appointment", appointmentSchema);

View File

@@ -0,0 +1,83 @@
const mongoose = require("mongoose");
/**
* Schema for Appointment Submissions
* Stores appointment requests from users
*/
const appointmentSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
appointmentDate: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
visaTypes: {
type: [String],
default: [],
},
status: {
type: String,
enum: ["pending", "confirmed", "completed", "cancelled"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
confirmedAt: {
type: Date,
default: null,
},
completedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
appointmentSubmissionSchema.index({ status: 1, createdAt: -1 });
appointmentSubmissionSchema.index({ email: 1 });
appointmentSubmissionSchema.index({ appointmentDate: 1 });
module.exports = mongoose.model("AppointmentSubmission", appointmentSubmissionSchema);

View File

@@ -145,6 +145,11 @@ const mapSchema = new mongoose.Schema(
trim: true, trim: true,
default: "", default: "",
}, },
embedUrl: {
type: String,
trim: true,
default: "",
},
tileLayer: { tileLayer: {
type: tileLayerSchema, type: tileLayerSchema,
required: true, required: true,
@@ -161,11 +166,16 @@ const formFieldSchema = new mongoose.Schema(
required: true, required: true,
trim: true, trim: true,
}, },
label: {
type: String,
trim: true,
default: "",
},
type: { type: {
type: String, type: String,
required: true, required: true,
trim: true, trim: true,
enum: ["text", "email", "tel", "textarea", "programme"], enum: ["text", "email", "tel", "textarea", "programme", "date"],
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -176,6 +186,11 @@ const formFieldSchema = new mongoose.Schema(
type: Boolean, type: Boolean,
default: false, default: false,
}, },
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
programmeName: { programmeName: {
type: String, type: String,
trim: true, trim: true,
@@ -193,6 +208,16 @@ const submitButtonSchema = new mongoose.Schema(
required: true, required: true,
trim: true, trim: true,
}, },
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn style-2",
},
}, },
{ _id: false } { _id: false }
); );
@@ -210,6 +235,11 @@ const formSchema = new mongoose.Schema(
trim: true, trim: true,
default: "", default: "",
}, },
description: {
type: String,
trim: true,
default: "",
},
fields: { fields: {
type: [formFieldSchema], type: [formFieldSchema],
default: [], default: [],
@@ -335,6 +365,7 @@ contactSchema.statics.migrateFromJson = async function (jsonData) {
zoom: jsonData.map?.zoom || 15, zoom: jsonData.map?.zoom || 15,
location: jsonData.map?.location || "", location: jsonData.map?.location || "",
markerTitle: jsonData.map?.markerTitle || "", markerTitle: jsonData.map?.markerTitle || "",
embedUrl: jsonData.map?.embedUrl || "",
tileLayer: { tileLayer: {
url: url:
jsonData.map?.tileLayer?.url || jsonData.map?.tileLayer?.url ||
@@ -347,15 +378,20 @@ contactSchema.statics.migrateFromJson = async function (jsonData) {
form: { form: {
sectionLabel: jsonData.form?.sectionLabel || "", sectionLabel: jsonData.form?.sectionLabel || "",
heading: jsonData.form?.heading || "", heading: jsonData.form?.heading || "",
description: jsonData.form?.description || "",
fields: (jsonData.form?.fields || []).map((field) => ({ fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "", name: field.name || "",
label: field.label || "",
type: field.type || "text", type: field.type || "text",
placeholder: field.placeholder || "", placeholder: field.placeholder || "",
required: field.required || false, required: field.required || false,
colClass: field.colClass || "col-lg-12",
programmeName: field.programmeName || "", programmeName: field.programmeName || "",
})), })),
submitButton: { submitButton: {
text: jsonData.form?.submitButton?.text || "Send Message", text: jsonData.form?.submitButton?.text || "Send Message",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn style-2",
}, },
}, },
}; };

View File

@@ -0,0 +1,74 @@
const mongoose = require("mongoose");
/**
* Schema for Contact Form Submissions
* Stores user inquiries from the contact form
*/
const contactSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
date: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
status: {
type: String,
enum: ["pending", "read", "replied", "archived"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
repliedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
contactSubmissionSchema.index({ status: 1, createdAt: -1 });
contactSubmissionSchema.index({ email: 1 });
module.exports = mongoose.model("ContactSubmission", contactSubmissionSchema);

328
models/pricing.js Normal file
View File

@@ -0,0 +1,328 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Pricing) {
delete mongoose.models.Pricing;
}
if (mongoose.connection.models.Pricing) {
delete mongoose.connection.models.Pricing;
}
// Schema for breadcrumb item
const breadcrumbItemSchema = new mongoose.Schema(
{
text: {
type: String,
trim: true,
default: "",
},
link: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Pricing Plan",
},
backgroundImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/breadcrumb.jpg",
},
shapeImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/shape.png",
},
breadcrumb: {
type: [breadcrumbItemSchema],
default: [],
},
},
{ _id: false }
);
// Schema for pricing section header
const pricingSectionSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "pricing plan",
},
heading: {
type: String,
trim: true,
default: "Flexible Plans to Suit Every Traveler",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for individual plan
const planSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: true,
},
price: {
type: String,
trim: true,
default: "0",
},
period: {
type: String,
trim: true,
default: "mo",
},
currency: {
type: String,
trim: true,
default: "$",
},
buttonText: {
type: String,
trim: true,
default: "Get Started Today",
},
buttonLink: {
type: String,
trim: true,
default: "/pricing",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
style: {
type: String,
trim: true,
enum: ["default", "style-2"],
default: "default",
},
features: {
type: [String],
default: [],
},
},
{ _id: false }
);
// Schema for plans container
const plansSchema = new mongoose.Schema(
{
monthly: {
type: [planSchema],
default: [],
},
yearly: {
type: [planSchema],
default: [],
},
},
{ _id: false }
);
// Schema for testimonial item
const testimonialItemSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
default: "",
},
role: {
type: String,
trim: true,
default: "",
},
rating: {
type: Number,
min: 1,
max: 5,
default: 5,
},
content: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for testimonials section
const testimonialsSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "What Our Clients Say",
},
heading: {
type: String,
trim: true,
default: "Immigration Success Stories",
},
buttonText: {
type: String,
trim: true,
default: "View All Review",
},
buttonLink: {
type: String,
trim: true,
default: "/contact",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
image: {
type: String,
trim: true,
default: "",
},
items: {
type: [testimonialItemSchema],
default: [],
},
},
{ _id: false }
);
// Main Pricing Schema
const pricingSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
pricingSection: {
type: pricingSectionSchema,
default: () => ({}),
},
plans: {
type: plansSchema,
default: () => ({}),
},
testimonials: {
type: testimonialsSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
pricingSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default pricing exists
const existingPricing = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Pricing Plan",
backgroundImage: jsonData.hero?.backgroundImage || "/assets/img/inner-page/breadcrumb.jpg",
shapeImage: jsonData.hero?.shapeImage || "/assets/img/inner-page/shape.png",
breadcrumb: (jsonData.hero?.breadcrumb || []).map((item) => ({
text: item.text || "",
link: item.link || "",
})),
},
pricingSection: {
subtitle: jsonData.pricingSection?.subtitle || "pricing plan",
heading: jsonData.pricingSection?.heading || "Flexible Plans to Suit Every Traveler",
description: jsonData.pricingSection?.description || "",
},
plans: {
monthly: (jsonData.plans?.monthly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
yearly: (jsonData.plans?.yearly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
},
testimonials: {
subtitle: jsonData.testimonials?.subtitle || "What Our Clients Say",
heading: jsonData.testimonials?.heading || "Immigration Success Stories",
buttonText: jsonData.testimonials?.buttonText || "View All Review",
buttonLink: jsonData.testimonials?.buttonLink || "/contact",
buttonIcon: jsonData.testimonials?.buttonIcon || "fa-solid fa-arrow-right",
image: jsonData.testimonials?.image || "",
items: (jsonData.testimonials?.items || []).map((item) => ({
name: item.name || "",
role: item.role || "",
rating: item.rating || 5,
content: item.content || "",
})),
},
};
if (existingPricing) {
// Update existing pricing
existingPricing.hero = processedData.hero;
existingPricing.pricingSection = processedData.pricingSection;
existingPricing.plans = processedData.plans;
existingPricing.testimonials = processedData.testimonials;
await existingPricing.save();
console.log("Pricing data updated successfully");
return existingPricing;
} else {
// Create new pricing
const newPricing = await this.create({
name: "default",
...processedData,
});
console.log("Pricing data imported successfully");
return newPricing;
}
} catch (error) {
console.error("Error migrating pricing data:", error);
throw error;
}
};
module.exports = mongoose.model("Pricing", pricingSchema);

View File

@@ -157,6 +157,52 @@ router.get(
contactController.getContactData, contactController.getContactData,
); );
// Contact submissions management
router.get(
"/contact/submissions",
ensureAuthenticated,
contactController.getSubmissions
);
router.put(
"/contact/submissions/:id",
ensureAuthenticated,
contactController.updateSubmissionStatus
);
// Appointment management
const appointmentController = require("../controllers/appointmentController");
router.get(
"/appointments",
ensureAuthenticated,
appointmentController.getAppointments
);
router.get(
"/appointments/:id",
ensureAuthenticated,
appointmentController.getAppointmentById
);
router.put(
"/appointments/:id",
ensureAuthenticated,
appointmentController.updateAppointmentStatus
);
router.delete(
"/appointments/:id",
ensureAuthenticated,
appointmentController.deleteAppointment
);
// Appointment CMS page management
router.get("/appointment", ensureAuthenticated, appointmentController.index);
router.post("/appointment/update", ensureAuthenticated, appointmentController.update);
router.get("/appointment/data", ensureAuthenticated, appointmentController.getAppointmentData);
// Pricing CMS page management
const pricingController = require("../controllers/pricingController");
router.get("/pricing", ensureAuthenticated, pricingController.index);
router.post("/pricing/update", ensureAuthenticated, pricingController.update);
router.get("/pricing/data", ensureAuthenticated, pricingController.getPricingData);
// Activity CRUD routes // Activity CRUD routes
router.get("/activity", ensureAuthenticated, activityController.index); router.get("/activity", ensureAuthenticated, activityController.index);
router.get( router.get(
@@ -355,9 +401,9 @@ router.post(
// Test Image Paths route // Test Image Paths route
router.get("/test-images", ensureAuthenticated, (req, res) => { router.get("/test-images", ensureAuthenticated, (req, res) => {
const fs = require("fs"); const fs = require('fs');
const path = require("path"); const path = require('path');
const campLocationData = require("../data/camp-location.json"); const campLocationData = require('../data/camp-location.json');
// Collect all image paths // Collect all image paths
const imagePaths = []; const imagePaths = [];
@@ -408,9 +454,9 @@ router.get("/test-images", ensureAuthenticated, (req, res) => {
}); });
} }
res.render("admin/test-images", { res.render('admin/test-images', {
layout: "layouts/admin", layout: 'layouts/admin',
title: "Test Image Paths", title: 'Test Image Paths',
images: imagePaths, images: imagePaths,
user: req.session.user, user: req.session.user,
}); });

View File

@@ -56,6 +56,18 @@ router.get("/api/footer", footerController.api);
// Contact API route // Contact API route
router.get("/api/contact", contactController.api); router.get("/api/contact", contactController.api);
// Contact form submission (public)
router.post("/api/contact/submit", contactController.submitForm);
// Appointment API
const appointmentController = require("../controllers/appointmentController");
router.get("/api/appointment", appointmentController.api);
router.post("/api/appointment/submit", appointmentController.submitAppointment);
// Pricing API
const pricingController = require("../controllers/pricingController");
router.get("/api/pricing", pricingController.api);
router.get("/api/faq", faqController.api); router.get("/api/faq", faqController.api);
// Safety API route // Safety API route
router.get("/api/safety", safetyController.api); router.get("/api/safety", safetyController.api);

View File

@@ -0,0 +1,38 @@
require("dotenv").config();
const fs = require("fs").promises;
const path = require("path");
const connectDB = require("../config/database");
const Contact = require("../models/contact");
const mongoose = require("mongoose");
/**
* Migration: contact
* Migrate contact data from contact-data.json
*/
async function migrate() {
try {
await connectDB();
// Read contact-data.json file
const contactJsonPath = path.join(__dirname, "../data/contact.json");
const contactData = JSON.parse(await fs.readFile(contactJsonPath, "utf8"));
// Migrate data using the model's static method
await Contact.migrateFromJson(contactData);
console.log("Contact migration completed successfully");
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error("Migration error:", error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,71 @@
/**
* Migration script for Appointment data
* Imports data from appointment.json to MongoDB
*
* Run: node scripts/2026_02_03_appointment.js
*/
require("dotenv").config();
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
// Connect to MongoDB
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log("MongoDB connected successfully");
} catch (error) {
console.error("MongoDB connection error:", error);
process.exit(1);
}
};
const runMigration = async () => {
try {
await connectDB();
// Load Appointment model
const Appointment = require("../models/appointment");
// Load JSON data
const jsonPath = path.join(__dirname, "../data/appointment.json");
if (!fs.existsSync(jsonPath)) {
console.log("appointment.json not found, creating default data...");
const defaultData = {
hero: {
title: "Make Appointment",
backgroundImage: "",
subtitle: "",
heading: "",
description: "",
},
visaOptions: [],
form: {
heading: "Request Appointment",
fields: [],
submitButton: {
text: "Request Appointment",
icon: "fa-solid fa-arrow-right",
buttonClass: "theme-btn",
},
},
};
await Appointment.migrateFromJson(defaultData);
} else {
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
console.log("Loaded appointment.json data");
await Appointment.migrateFromJson(jsonData);
}
console.log("✅ Appointment migration completed successfully!");
} catch (error) {
console.error("❌ Migration failed:", error);
} finally {
await mongoose.connection.close();
console.log("MongoDB connection closed");
}
};
runMigration();

View File

@@ -42,8 +42,6 @@ app.use(
}, },
express.static(path.join(__dirname, "assets")), express.static(path.join(__dirname, "assets")),
); );
app.use("/img", express.static(path.join(process.cwd(), "public/img")));
// Serve static files from public directory (uploads, etc.)
app.use( app.use(
"/uploads", "/uploads",
(req, res, next) => { (req, res, next) => {
@@ -55,6 +53,11 @@ app.use(
express.static(path.join(__dirname, "public", "uploads")), express.static(path.join(__dirname, "public", "uploads")),
); );
// Serve other public files
app.use(
express.static(path.join(__dirname, "public")),
);
// Serve other public files // Serve other public files
app.use(express.static(path.join(__dirname, "public"))); app.use(express.static(path.join(__dirname, "public")));
// Session configuration // Session configuration

View File

@@ -0,0 +1,791 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on Appointment page</p>
</div>
<div>
<a href="<%= frontendUrl %>/make-appointment/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Appointment Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="appointmentForm"
action="/admin/appointment/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="visaOptions" id="visaOptionsJson">
<input type="hidden" name="form" id="formJson">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#visaOptions" role="tab">
<i class="fas fa-passport me-2"></i>Visa Options
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
<i class="fas fa-envelope me-2"></i>Form
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#submissions" role="tab">
<i class="fas fa-list me-2"></i>Submissions
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Hero Section</h6>
<div class="row g-3">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="appointment">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px;">
<% if (data.hero?.backgroundImage) { %>
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
!heroImgSrc.startsWith('http://') &&
!heroImgSrc.startsWith('https://')) {
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
heroImgSrc; } %>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="heroSubtitle"
name="heroSubtitle" value="<%= data.hero?.subtitle || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="heroHeading"
name="heroHeading" value="<%= data.hero?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="heroDescription"
name="heroDescription"
rows="2"><%= data.hero?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Visa Options Tab -->
<div class="tab-pane fade" id="visaOptions" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Visa Options</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addVisaOption()">
<i class="fas fa-plus"></i> Add Option
</button>
</div>
<p class="text-muted small">These options will appear in the visa type selection
dropdown on the appointment form.</p>
<div id="visaOptionsContainer">
<% if (data.visaOptions && data.visaOptions.length> 0) { %>
<% data.visaOptions.forEach((option, index)=> { %>
<div class="input-group mb-2 visa-option-item">
<span class="input-group-text"><i
class="fas fa-passport"></i></span>
<input type="text" class="form-control visa-option-input"
value="<%= option %>" placeholder="Enter visa option">
<button type="button" class="btn btn-outline-danger"
onclick="removeVisaOption(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Form Tab -->
<div class="tab-pane fade" id="form" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Form Settings</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Form Heading</label>
<input type="text" class="form-control" id="formHeading"
value="<%= data.form?.heading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Submit Button Text</label>
<input type="text" class="form-control" id="formSubmitButtonText"
value="<%= data.form?.submitButton?.text || 'Request Appointment' %>">
</div>
<!-- Hidden fields for submitButton icon and buttonClass -->
<input type="hidden" id="formSubmitButtonIcon"
value="<%= data.form?.submitButton?.icon || 'fa-solid fa-arrow-right' %>">
<input type="hidden" id="formSubmitButtonClass"
value="<%= data.form?.submitButton?.buttonClass || 'theme-btn' %>">
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Form Fields</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addFormField()">
<i class="fas fa-plus"></i> Add Field
</button>
</div>
<div id="formFieldsContainer">
<% if (data.form?.fields && data.form.fields.length> 0) { %>
<% data.form.fields.forEach((field, index)=> { %>
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text"
class="form-control field-name-input"
value="<%= field.name || '' %>"
placeholder="e.g., name">
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text"
class="form-control field-label-input"
value="<%= field.label || '' %>"
placeholder="e.g., Your Name">
</div>
<div class="col-md-2">
<label class="form-label">Type</label>
<select class="form-select field-type-select">
<option value="text" <%=field.type==='text'
? 'selected' : '' %>>Text</option>
<option value="email" <%=field.type==='email'
? 'selected' : '' %>>Email</option>
<option value="tel" <%=field.type==='tel'
? 'selected' : '' %>>Phone</option>
<option value="textarea"
<%=field.type==='textarea' ? 'selected' : ''
%>>Textarea</option>
<option value="date" <%=field.type==='date'
? 'selected' : '' %>>Date</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Col Class</label>
<select class="form-select field-col-select">
<option value="col-lg-4"
<%=field.colClass==='col-lg-4' ? 'selected'
: '' %>>1/3 Width</option>
<option value="col-lg-6"
<%=field.colClass==='col-lg-6' ? 'selected'
: '' %>>1/2 Width</option>
<option value="col-lg-12"
<%=field.colClass==='col-lg-12' ? 'selected'
: '' %>>Full Width</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input
class="form-check-input field-required-check"
type="checkbox" <%=field.required
? 'checked' : '' %>>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text"
class="form-control field-placeholder-input"
value="<%= field.placeholder || '' %>">
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Submissions Tab -->
<div class="tab-pane fade" id="submissions" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Recent Submissions</h6>
</div>
<!-- Date Filter -->
<div class="row g-2 mb-4 align-items-end" id="filterContainer">
<input type="hidden" id="filterTab" value="submissions">
<div class="col-md-3">
<label class="form-label small text-muted">Start Date</label>
<input type="date" class="form-control form-control-sm"
id="filterStartDate" value="<%= locals.startDate || '' %>">
</div>
<div class="col-md-3">
<label class="form-label small text-muted">End Date</label>
<input type="date" class="form-control form-control-sm"
id="filterEndDate" value="<%= locals.endDate || '' %>">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-sm btn-primary w-100"
onclick="applyDateFilter()">
<i class="fas fa-filter me-1"></i> Filter
</button>
</div>
<div class="col-md-2">
<a href="/admin/appointment?tab=submissions"
class="btn btn-sm btn-outline-secondary w-100">
<i class="fas fa-times me-1"></i> Clear
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Name</th>
<th>Contact</th>
<th>Appt Date</th>
<th>Visa Types</th>
<th>Message</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% if (locals.submissions && submissions.length> 0) { %>
<% submissions.forEach(submission=> { %>
<tr>
<td>
<%= new
Date(submission.createdAt).toLocaleDateString()
%>
<br>
<small class="text-muted">
<%= new
Date(submission.createdAt).toLocaleTimeString([],
{hour: '2-digit' , minute:'2-digit'}) %>
</small>
</td>
<td>
<%= submission.name %>
</td>
<td>
<div class="d-flex flex-column">
<a href="mailto:<%= submission.email %>"
class="text-decoration-none"><i
class="fas fa-envelope me-1"></i>
<%= submission.email %>
</a>
<% if(submission.phone) { %>
<span class="text-muted small"><i
class="fas fa-phone me-1"></i>
<%= submission.phone %>
</span>
<% } %>
</div>
</td>
<td>
<%= submission.appointmentDate || '-' %>
</td>
<td>
<% if (submission.visaTypes &&
submission.visaTypes.length> 0) { %>
<% submission.visaTypes.forEach(type=> { %>
<span
class="badge bg-light text-dark border me-1">
<%= type %>
</span>
<% }); %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (submission.message) { %>
<div title="<%= submission.message %>"
style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<%= submission.message %>
</div>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% let statusClass='bg-secondary' ;
if(submission.status==='pending' )
statusClass='bg-warning text-dark' ;
if(submission.status==='confirmed' )
statusClass='bg-success' ;
if(submission.status==='completed' )
statusClass='bg-info text-dark' ;
if(submission.status==='cancelled' )
statusClass='bg-danger' ; %>
<span
class="badge <%= statusClass %> rounded-pill">
<%= submission.status %>
</span>
</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
onclick="openStatusModal('<%= submission._id %>', '<%= submission.status %>')"
title="Update Status">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="8"
class="text-center py-4 text-muted">No
submissions found</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<div class="mt-3 text-end">
<small class="text-muted">Showing last 50 submissions</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Status Update Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="statusForm">
<input type="hidden" id="statusSubmissionId">
<div class="mb-3">
<label for="statusSelect" class="form-label">Status</label>
<select class="form-select" id="statusSelect">
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="saveStatus()">Save changes</button>
</div>
</div>
</div>
</div>
<script type="application/json" id="appointmentDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
let statusModal = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('appointmentDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
// Check for tab parameter in URL
const urlParams = new URLSearchParams(window.location.search);
const tab = urlParams.get('tab');
if (tab) {
const triggerEl = document.querySelector(`a[href="#${tab}"]`);
if (triggerEl) {
const tabInstance = new bootstrap.Tab(triggerEl);
tabInstance.show();
}
}
// Move modal to body to prevent backdrop issues
const statusModalEl = document.getElementById('statusModal');
if (statusModalEl) {
document.body.appendChild(statusModalEl);
}
statusModal = new bootstrap.Modal(statusModalEl);
updateAllJsonInputs();
initializeFormHandlers();
});
function applyDateFilter() {
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
const url = new URL(window.location.href);
url.searchParams.set('tab', 'submissions');
if (startDate) {
url.searchParams.set('startDate', startDate);
} else {
url.searchParams.delete('startDate');
}
if (endDate) {
url.searchParams.set('endDate', endDate);
} else {
url.searchParams.delete('endDate');
}
window.location.href = url.toString();
}
function openStatusModal(id, currentStatus) {
document.getElementById('statusSubmissionId').value = id;
document.getElementById('statusSelect').value = currentStatus;
statusModal.show();
}
async function saveStatus() {
const id = document.getElementById('statusSubmissionId').value;
const status = document.getElementById('statusSelect').value;
try {
const response = await fetch(`/admin/appointments/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ status })
});
const result = await response.json();
if (result.success) {
// Determine CSS class for the notification or badge
// Since this is generic, we'll reload or update UI manually if complex.
// Reload is safest to show updated table state (including sorting/filtering if any)
// But let's try to be smooth:
window.location.reload();
} else {
alert('Failed to update status: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Error updating status:', error);
alert('Error updating status');
}
}
function initializeFormHandlers() {
const form = document.getElementById('appointmentForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Image upload buttons
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
// Update preview when background image changes
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
updateHeroImagePreview(this.value);
});
}
function updateHeroImagePreview(imagePath) {
const previewContainer = document.getElementById('heroImagePreview');
if (imagePath) {
let imgSrc = imagePath;
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
}
previewContainer.innerHTML = `
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewContainer.innerHTML = `
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
function updateAllJsonInputs() {
updateJsonData();
}
function updateJsonData() {
// Hero data
const heroData = {
title: document.getElementById('heroTitle').value || '',
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
subtitle: document.getElementById('heroSubtitle').value || '',
heading: document.getElementById('heroHeading').value || '',
description: document.getElementById('heroDescription').value || '',
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Visa options
const visaOptions = [];
document.querySelectorAll('.visa-option-input').forEach(input => {
if (input.value.trim()) {
visaOptions.push(input.value.trim());
}
});
document.getElementById('visaOptionsJson').value = JSON.stringify(visaOptions);
// Form data
const fields = [];
document.querySelectorAll('.form-field-item').forEach(item => {
fields.push({
name: item.querySelector('.field-name-input').value || '',
label: item.querySelector('.field-label-input').value || '',
type: item.querySelector('.field-type-select').value || 'text',
placeholder: item.querySelector('.field-placeholder-input').value || '',
required: item.querySelector('.field-required-check').checked,
colClass: item.querySelector('.field-col-select').value || 'col-lg-12',
});
});
const formData = {
heading: document.getElementById('formHeading').value || '',
fields: fields,
submitButton: {
text: document.getElementById('formSubmitButtonText').value || 'Request Appointment',
icon: document.getElementById('formSubmitButtonIcon').value || 'fa-solid fa-arrow-right',
buttonClass: document.getElementById('formSubmitButtonClass').value || 'theme-btn',
},
};
document.getElementById('formJson').value = JSON.stringify(formData);
}
function addVisaOption() {
const container = document.getElementById('visaOptionsContainer');
const html = `
<div class="input-group mb-2 visa-option-item">
<span class="input-group-text"><i class="fas fa-passport"></i></span>
<input type="text" class="form-control visa-option-input" value="" placeholder="Enter visa option">
<button type="button" class="btn btn-outline-danger" onclick="removeVisaOption(this)">
<i class="fas fa-trash"></i>
</button>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeVisaOption(button) {
button.closest('.visa-option-item').remove();
}
function addFormField() {
const container = document.getElementById('formFieldsContainer');
const html = `
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text" class="form-control field-name-input" value="" placeholder="e.g., name">
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text" class="form-control field-label-input" value="" placeholder="e.g., Your Name">
</div>
<div class="col-md-2">
<label class="form-label">Type</label>
<select class="form-select field-type-select">
<option value="text" selected>Text</option>
<option value="email">Email</option>
<option value="tel">Phone</option>
<option value="textarea">Textarea</option>
<option value="date">Date</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Col Class</label>
<select class="form-select field-col-select">
<option value="col-lg-4">1/3 Width</option>
<option value="col-lg-6">1/2 Width</option>
<option value="col-lg-12" selected>Full Width</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input class="form-check-input field-required-check" type="checkbox">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control field-placeholder-input" value="">
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFormField(button) {
button.closest('.form-field-item').remove();
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function (reuse from shared)
function openImageUploader(targetInput, imageType) {
// Open upload modal or trigger file input
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('/admin/upload/image', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.imagePath) {
document.getElementById(targetInput).value = result.imagePath;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.imagePath);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -11,18 +11,9 @@
<div class="col-md-4 border-end"> <div class="col-md-4 border-end">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-home fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-home fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Home</h5> <h5 class="mb-0">Home</h5>
@@ -38,18 +29,9 @@
<div class="col-md-4 border-end"> <div class="col-md-4 border-end">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-bars fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-bars fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Header & Menu</h5> <h5 class="mb-0">Header & Menu</h5>
@@ -65,18 +47,9 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-window-minimize fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-window-minimize fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Footer</h5> <h5 class="mb-0">Footer</h5>
@@ -92,18 +65,9 @@
<div class="col-md-4 border-end border-top"> <div class="col-md-4 border-end border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-users fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-users fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">About Us</h5> <h5 class="mb-0">About Us</h5>
@@ -119,18 +83,9 @@
<div class="col-md-4 border-end border-top"> <div class="col-md-4 border-end border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-envelope fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-envelope fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Contact</h5> <h5 class="mb-0">Contact</h5>
@@ -146,18 +101,9 @@
<div class="col-md-4 border-top"> <div class="col-md-4 border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-question-circle fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-question-circle fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">FAQ</h5> <h5 class="mb-0">FAQ</h5>
@@ -173,18 +119,45 @@
<div class="col-md-4 border-end border-top"> <div class="col-md-4 border-end border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-calendar-check fa-lg" style="color: var(--primary-color);"></i>
width: 50px; </div>
height: 50px; <div>
background-color: rgba(184, 183, 106, 0.1); <h5 class="mb-0">Appointment</h5>
" <p class="text-muted mb-0 small">Manage appointment page</p>
> </div>
<i </div>
class="fas fa-file-contract fa-lg" <a href="/admin/appointment" class="btn btn-sm btn-primary w-100 mt-2">
style="color: var(--primary-color)" <i class="fas fa-edit me-2"></i>Edit
></i> </a>
</div>
</div>
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-tags fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Pricing</h5>
<p class="text-muted mb-0 small">Manage pricing page</p>
</div>
</div>
<a href="/admin/pricing" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-file-contract fa-lg" style="color: var(--primary-color);"></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Terms & Conditions</h5> <h5 class="mb-0">Terms & Conditions</h5>
@@ -203,18 +176,9 @@
<div class="col-md-4 border-end border-top"> <div class="col-md-4 border-end border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-plane fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-plane fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Travel</h5> <h5 class="mb-0">Travel</h5>
@@ -230,18 +194,9 @@
<div class="col-md-4 border-top"> <div class="col-md-4 border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-shield-alt fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-shield-alt fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Safety</h5> <h5 class="mb-0">Safety</h5>
@@ -257,18 +212,9 @@
<div class="col-md-4 border-end border-top"> <div class="col-md-4 border-end border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-campground fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-campground fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Camp Location</h5> <h5 class="mb-0">Camp Location</h5>
@@ -287,18 +233,9 @@
<div class="col-md-4 border-end border-top"> <div class="col-md-4 border-end border-top">
<div class="p-4"> <div class="p-4">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-running fa-lg" style="color: var(--primary-color);"></i>
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-running fa-lg"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<h5 class="mb-0">Activities</h5> <h5 class="mb-0">Activities</h5>
@@ -362,18 +299,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-bars" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-bars"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Menu Header API</span> <span>Menu Header API</span>
</div> </div>
@@ -400,18 +328,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-home" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-home"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Home API</span> <span>Home API</span>
</div> </div>
@@ -438,18 +357,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-info-circle"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>About API</span> <span>About API</span>
</div> </div>
@@ -476,18 +386,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-users" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-users"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>About Us API</span> <span>About Us API</span>
</div> </div>
@@ -514,18 +415,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-question-circle" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-question-circle"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>FAQ API</span> <span>FAQ API</span>
</div> </div>
@@ -552,18 +444,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-file-contract" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-file-contract"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Terms & Conditions API</span> <span>Terms & Conditions API</span>
</div> </div>
@@ -590,18 +473,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-plane" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-plane"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Travel API</span> <span>Travel API</span>
</div> </div>
@@ -628,18 +502,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-shield-alt" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-shield-alt"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Safety API</span> <span>Safety API</span>
</div> </div>
@@ -666,18 +531,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-campground" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-campground"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Camp Location API</span> <span>Camp Location API</span>
</div> </div>
@@ -705,18 +561,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-sitemap" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-sitemap"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Menu Tree API</span> <span>Menu Tree API</span>
</div> </div>
@@ -743,18 +590,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-envelope" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-envelope"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Contact API</span> <span>Contact API</span>
</div> </div>
@@ -781,56 +619,9 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-2"
class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-campground" style="color: var(--primary-color);"></i>
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-envelope"
style="color: var(--primary-color)"
></i>
</div>
<span>Service API</span>
</div>
</td>
<td><code>/api/service</code></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get service data</td>
<td>
<a
href="/api/service"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div
class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="
width: 32px;
height: 32px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-campground"
style="color: var(--primary-color)"
></i>
</div> </div>
<span>Camp Location API</span> <span>Camp Location API</span>
</div> </div>
@@ -875,18 +666,9 @@
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
width: 40px;
height: 40px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-info-circle"
style="color: var(--primary-color)"
></i>
</div> </div>
<div> <div>
<div class="text-muted small">Version</div> <div class="text-muted small">Version</div>
@@ -899,19 +681,13 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div <div class="rounded-circle d-flex align-items-center justify-content-center me-3"
class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
style=" <i class="fas fa-user" style="color: var(--primary-color);"></i>
width: 40px;
height: 40px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i class="fas fa-user" style="color: var(--primary-color)"></i>
</div> </div>
<div> <div>
<div class="text-muted small">Logged in as</div> <div class="text-muted small">Logged in as</div>
<div class="fw-bold" style="color: var(--primary-color)"> <div class="fw-bold" style="color: var(--primary-color);">
<%= user.username %> <%= user.username %>
</div> </div>
</div> </div>
@@ -919,14 +695,8 @@
</div> </div>
</div> </div>
<div <div class="alert mt-3 mb-0"
class="alert mt-3 mb-0" style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
style="
background-color: rgba(184, 183, 106, 0.05);
border-left: 4px solid var(--primary-color);
"
role="alert"
>
<div class="d-flex"> <div class="d-flex">
<div class="me-3"> <div class="me-3">
<i <i

View File

@@ -0,0 +1,742 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on Pricing page</p>
</div>
<div>
<a href="<%= frontendUrl %>/pricing/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Pricing Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="pricingForm" action="/admin/pricing/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="pricingSection" id="pricingSectionJson">
<input type="hidden" name="plans" id="plansJson">
<input type="hidden" name="testimonials" id="testimonialsJson">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#pricingSection" role="tab">
<i class="fas fa-tags me-2"></i>Pricing Section
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#plans" role="tab">
<i class="fas fa-dollar-sign me-2"></i>Plans
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
<i class="fas fa-quote-right me-2"></i>Testimonials
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Hero Section</h6>
<div class="row g-3">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="pricing">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px;">
<% if (data.hero?.backgroundImage) { %>
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
!heroImgSrc.startsWith('http://') &&
!heroImgSrc.startsWith('https://')) {
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
heroImgSrc; } %>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Section Tab -->
<div class="tab-pane fade" id="pricingSection" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Pricing Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="pricingSectionSubtitle"
value="<%= data.pricingSection?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="pricingSectionHeading"
value="<%= data.pricingSection?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="pricingSectionDescription"
rows="3"><%= data.pricingSection?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Plans Tab -->
<div class="tab-pane fade" id="plans" role="tabpanel">
<!-- Monthly Plans -->
<div class="card border shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Monthly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('monthly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="monthlyPlansContainer">
<% if (data.plans?.monthly && data.plans.monthly.length> 0) { %>
<% data.plans.monthly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="monthly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<!-- Yearly Plans -->
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Yearly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('yearly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="yearlyPlansContainer">
<% if (data.plans?.yearly && data.plans.yearly.length> 0) { %>
<% data.plans.yearly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="yearly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Testimonials Tab -->
<div class="tab-pane fade" id="testimonials" role="tabpanel">
<div class="card border shadow-sm mb-4">
<div class="card-body">
<h6 class="fw-medium mb-3">Testimonials Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="testimonialsSubtitle"
value="<%= data.testimonials?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Text</label>
<input type="text" class="form-control" id="testimonialsButtonText"
value="<%= data.testimonials?.buttonText || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Link</label>
<input type="text" class="form-control" id="testimonialsButtonLink"
value="<%= data.testimonials?.buttonLink || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Section Image</label>
<div class="input-group">
<input type="text" class="form-control" id="testimonialsImage"
value="<%= data.testimonials?.image || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsImage" data-image-type="pricing">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Testimonial Items</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addTestimonial()">
<i class="fas fa-plus"></i> Add Testimonial
</button>
</div>
<div id="testimonialsContainer">
<% if (data.testimonials?.items && data.testimonials.items.length> 0) { %>
<% data.testimonials.items.forEach((item, index)=> { %>
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text"
class="form-control testimonial-name"
value="<%= item.name || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text"
class="form-control testimonial-role"
value="<%= item.role || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1" <%=item.rating===1
? 'selected' : '' %>>1 Star</option>
<option value="2" <%=item.rating===2
? 'selected' : '' %>>2 Stars</option>
<option value="3" <%=item.rating===3
? 'selected' : '' %>>3 Stars</option>
<option value="4" <%=item.rating===4
? 'selected' : '' %>>4 Stars</option>
<option value="5" <%=item.rating===5
? 'selected' : '' %>>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content"
rows="3"><%= item.content || '' %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="pricingDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('pricingDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
updateAllJsonInputs();
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('pricingForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Image upload buttons
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
// Update preview when background image changes
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
updateHeroImagePreview(this.value);
});
}
function updateHeroImagePreview(imagePath) {
const previewContainer = document.getElementById('heroImagePreview');
if (imagePath) {
let imgSrc = imagePath;
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
}
previewContainer.innerHTML = `
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewContainer.innerHTML = `
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
function updateAllJsonInputs() {
updateJsonData();
}
function updateJsonData() {
// Hero data
const heroData = {
title: document.getElementById('heroTitle').value || '',
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
shapeImage: originalFormData?.hero?.shapeImage || '/assets/img/inner-page/shape.png',
breadcrumb: originalFormData?.hero?.breadcrumb || [],
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Pricing Section data
const pricingSectionData = {
subtitle: document.getElementById('pricingSectionSubtitle').value || '',
heading: document.getElementById('pricingSectionHeading').value || '',
description: document.getElementById('pricingSectionDescription').value || '',
};
document.getElementById('pricingSectionJson').value = JSON.stringify(pricingSectionData);
// Plans data
const monthlyPlans = [];
document.querySelectorAll('#monthlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
monthlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
const yearlyPlans = [];
document.querySelectorAll('#yearlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
yearlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
document.getElementById('plansJson').value = JSON.stringify({
monthly: monthlyPlans,
yearly: yearlyPlans,
});
// Testimonials data
const testimonialItems = [];
document.querySelectorAll('#testimonialsContainer .testimonial-item').forEach(item => {
testimonialItems.push({
name: item.querySelector('.testimonial-name').value || '',
role: item.querySelector('.testimonial-role').value || '',
rating: parseInt(item.querySelector('.testimonial-rating').value) || 5,
content: item.querySelector('.testimonial-content').value || '',
});
});
const testimonialsData = {
subtitle: document.getElementById('testimonialsSubtitle').value || '',
heading: document.getElementById('testimonialsHeading').value || '',
buttonText: document.getElementById('testimonialsButtonText').value || '',
buttonLink: document.getElementById('testimonialsButtonLink').value || '',
buttonIcon: originalFormData?.testimonials?.buttonIcon || 'fa-solid fa-arrow-right',
image: document.getElementById('testimonialsImage').value || '',
items: testimonialItems,
};
document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData);
}
function addPlan(type) {
const container = document.getElementById(type + 'PlansContainer');
const html = `
<div class="card mb-3 plan-item" data-type="${type}">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name" value="">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price" value="">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text" class="form-control plan-currency" value="$">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period" value="mo">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default" selected>Default</option>
<option value="style-2">Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text" class="form-control plan-button-text" value="Get Started Today">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text" class="form-control plan-button-link" value="/pricing">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text" class="form-control plan-button-icon" value="fa-solid fa-arrow-right">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per line)</label>
<textarea class="form-control plan-features" rows="4"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removePlan(button) {
if (confirm('Are you sure you want to remove this plan?')) {
button.closest('.plan-item').remove();
}
}
function addTestimonial() {
const container = document.getElementById('testimonialsContainer');
const html = `
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control testimonial-name" value="">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text" class="form-control testimonial-role" value="">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1">1 Star</option>
<option value="2">2 Stars</option>
<option value="3">3 Stars</option>
<option value="4">4 Stars</option>
<option value="5" selected>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content" rows="3"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeTestimonial(button) {
if (confirm('Are you sure you want to remove this testimonial?')) {
button.closest('.testimonial-item').remove();
}
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function
function openImageUploader(targetInput, imageType) {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
// Send imageType via query string as controller expects req.query.imageType
const uploadUrl = '/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general');
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.path) {
document.getElementById(targetInput).value = result.path;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.path);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<link rel="icon" type="image/png" href="/uploads/layout/favicon.png" /> <link rel="icon" type="image/png" href="/uploads/layout/favicon.png" />
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -8,15 +9,9 @@
<%= typeof title !=='undefined' ? title + ' | ' : '' %>CMS-GGCamp <%= typeof title !=='undefined' ? title + ' | ' : '' %>CMS-GGCamp
</title> </title>
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<!-- Font Awesome --> <!-- Font Awesome -->
<link <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<style> <style>
:root { :root {
--primary-color: #b8b76a; --primary-color: #b8b76a;
@@ -74,6 +69,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/admin/contact">Contact</a> <a class="nav-link" href="/admin/contact">Contact</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/admin/appointment">Appointment</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/admin/faq">FAQ</a> <a class="nav-link" href="/admin/faq">FAQ</a>
</li> </li>
@@ -100,12 +98,10 @@
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4"> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<!-- Flash Messages Data (Hidden) --> <!-- Flash Messages Data (Hidden) -->
<% if(typeof success_msg !=='undefined' || typeof error_msg <% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %>
!=='undefined' || typeof error !=='undefined' ) { %>
<div id="flash-messages-data" style="display: none"> <div id="flash-messages-data" style="display: none">
<%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' <%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' && success_msg ? success_msg : null,
&& success_msg ? success_msg : null, error_msg: typeof error_msg error_msg: typeof error_msg !=='undefined' && error_msg ? error_msg : null, error: typeof error
!=='undefined' && error_msg ? error_msg : null, error: typeof error
!=='undefined' && error ? error : null }) %> !=='undefined' && error ? error : null }) %>
</div> </div>
<% } %> <% } %>
@@ -119,4 +115,5 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<%- script %> <%- script %>
</body> </body>
</html> </html>

View File

@@ -729,34 +729,55 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a <a class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>" href="/admin/about-us">About us</a>
href="/admin/about-us"
>About us</a
>
</li> </li>
<li> <li>
<a <a class="dropdown-item <%= currentPath === '/admin/safety' ? 'active' : '' %>"
class="dropdown-item <%= currentPath === '/admin/safety' ? 'active' : '' %>" href="/admin/safety">Safety</a>
href="/admin/safety"
>Safety</a
>
</li> </li>
<li> <li>
<a <a class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>" href="/admin/faq">FAQ</a>
class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>"
href="/admin/faq"
>FAQ</a
>
</li> </li>
<li> <li>
<a <a class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>"
class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>" href="/admin/insurance">Insurance</a>
href="/admin/insurance"
>Insurance</a
>
</li> </li>
<li>
<a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
href="/admin/travel">Travel</a>
</li>
<li>
<a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
href="/admin/terms-conditions">Terms & Conditions</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
Us</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>"
href="/admin/appointment">Appointment</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/pricing' ? 'active' : '' %>"
href="/admin/pricing">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
href="/admin/camp-location">Camp Location</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>" href="/admin/activity">Activity
& Booking</a>
</li>
</ul>
<li> <li>
<a <a
class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>" class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"