init commit

This commit is contained in:
2025-10-19 18:27:00 +09:00
commit 150891b29d
219 changed files with 70016 additions and 0 deletions

View File

@@ -0,0 +1,387 @@
const express = require('express');
const router = express.Router();
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
const Portfolio = require('../models/Portfolio');
const Service = require('../models/Service');
const Contact = require('../models/Contact');
const SiteSettings = require('../models/SiteSettings');
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.redirect('/admin/login');
}
next();
};
// Admin login page
router.get('/login', (req, res) => {
if (req.session.user) {
return res.redirect('/admin/dashboard');
}
res.render('admin/login', {
title: 'Admin Login - SmartSolTech',
layout: 'admin/layout',
error: null
});
});
// Admin login POST
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email, isActive: true });
if (!user || !(await user.comparePassword(password))) {
return res.render('admin/login', {
title: 'Admin Login - SmartSolTech',
layout: 'admin/layout',
error: 'Invalid email or password'
});
}
await user.updateLastLogin();
req.session.user = {
id: user._id,
email: user.email,
name: user.name,
role: user.role
};
res.redirect('/admin/dashboard');
} catch (error) {
console.error('Admin login error:', error);
res.render('admin/login', {
title: 'Admin Login - SmartSolTech',
layout: 'admin/layout',
error: 'An error occurred. Please try again.'
});
}
});
// Admin logout
router.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
console.error('Logout error:', err);
}
res.redirect('/admin/login');
});
});
// Dashboard
router.get('/dashboard', requireAuth, async (req, res) => {
try {
const [
portfolioCount,
servicesCount,
contactsCount,
recentContacts,
recentPortfolio
] = await Promise.all([
Portfolio.countDocuments({ isPublished: true }),
Service.countDocuments({ isActive: true }),
Contact.countDocuments(),
Contact.find().sort({ createdAt: -1 }).limit(5),
Portfolio.find({ isPublished: true }).sort({ createdAt: -1 }).limit(5)
]);
const stats = {
portfolio: portfolioCount,
services: servicesCount,
contacts: contactsCount,
unreadContacts: await Contact.countDocuments({ isRead: false })
};
res.render('admin/dashboard', {
title: 'Dashboard - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
stats,
recentContacts,
recentPortfolio
});
} catch (error) {
console.error('Dashboard error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading dashboard'
});
}
});
// Portfolio management
router.get('/portfolio', requireAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const [portfolio, total] = await Promise.all([
Portfolio.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Portfolio.countDocuments()
]);
const totalPages = Math.ceil(total / limit);
res.render('admin/portfolio/list', {
title: 'Portfolio Management - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
portfolio,
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Portfolio list error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading portfolio'
});
}
});
// Add portfolio item
router.get('/portfolio/add', requireAuth, (req, res) => {
res.render('admin/portfolio/add', {
title: 'Add Portfolio Item - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
// Edit portfolio item
router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio) {
return res.status(404).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Portfolio item not found'
});
}
res.render('admin/portfolio/edit', {
title: 'Edit Portfolio Item - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
portfolio
});
} catch (error) {
console.error('Portfolio edit error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading portfolio item'
});
}
});
// Services management
router.get('/services', requireAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const [services, total] = await Promise.all([
Service.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Service.countDocuments()
]);
const totalPages = Math.ceil(total / limit);
res.render('admin/services/list', {
title: 'Services Management - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
services,
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Services list error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading services'
});
}
});
// Add service
router.get('/services/add', requireAuth, (req, res) => {
res.render('admin/services/add', {
title: 'Add Service - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
// Edit service
router.get('/services/edit/:id', requireAuth, async (req, res) => {
try {
const service = await Service.findById(req.params.id)
.populate('portfolio', 'title');
if (!service) {
return res.status(404).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Service not found'
});
}
const availablePortfolio = await Portfolio.find({ isPublished: true })
.select('title category');
res.render('admin/services/edit', {
title: 'Edit Service - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
service,
availablePortfolio
});
} catch (error) {
console.error('Service edit error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading service'
});
}
});
// Contacts management
router.get('/contacts', requireAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const status = req.query.status;
let query = {};
if (status && status !== 'all') {
query.status = status;
}
const [contacts, total] = await Promise.all([
Contact.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Contact.countDocuments(query)
]);
const totalPages = Math.ceil(total / limit);
res.render('admin/contacts/list', {
title: 'Contacts Management - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
contacts,
currentStatus: status || 'all',
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Contacts list error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading contacts'
});
}
});
// View contact details
router.get('/contacts/:id', requireAuth, async (req, res) => {
try {
const contact = await Contact.findById(req.params.id);
if (!contact) {
return res.status(404).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Contact not found'
});
}
// Mark as read
if (!contact.isRead) {
contact.isRead = true;
await contact.save();
}
res.render('admin/contacts/view', {
title: 'Contact Details - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
contact
});
} catch (error) {
console.error('Contact view error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading contact'
});
}
});
// Settings
router.get('/settings', requireAuth, async (req, res) => {
try {
const settings = await SiteSettings.findOne() || new SiteSettings();
res.render('admin/settings', {
title: 'Site Settings - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
settings
});
} catch (error) {
console.error('Settings error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading settings'
});
}
});
// Media gallery
router.get('/media', requireAuth, (req, res) => {
res.render('admin/media', {
title: 'Media Gallery - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
module.exports = router;

View File

@@ -0,0 +1,387 @@
const express = require('express');
const router = express.Router();
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
const Portfolio = require('../models/Portfolio');
const Service = require('../models/Service');
const Contact = require('../models/Contact');
const SiteSettings = require('../models/SiteSettings');
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.redirect('/admin/login');
}
next();
};
// Admin login page
router.get('/login', (req, res) => {
if (req.session.user) {
return res.redirect('/admin/dashboard');
}
res.render('admin/login', {
title: 'Admin Login - SmartSolTech',
layout: 'admin/layout',
error: null
});
});
// Admin login POST
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email, isActive: true });
if (!user || !(await user.comparePassword(password))) {
return res.render('admin/login', {
title: 'Admin Login - SmartSolTech',
layout: 'admin/layout',
error: 'Invalid email or password'
});
}
await user.updateLastLogin();
req.session.user = {
id: user._id,
email: user.email,
name: user.name,
role: user.role
};
res.redirect('/admin/dashboard');
} catch (error) {
console.error('Admin login error:', error);
res.render('admin/login', {
title: 'Admin Login - SmartSolTech',
layout: 'admin/layout',
error: 'An error occurred. Please try again.'
});
}
});
// Admin logout
router.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
console.error('Logout error:', err);
}
res.redirect('/admin/login');
});
});
// Dashboard
router.get('/dashboard', requireAuth, async (req, res) => {
try {
const [
portfolioCount,
servicesCount,
contactsCount,
recentContacts,
recentPortfolio
] = await Promise.all([
Portfolio.countDocuments({ isPublished: true }),
Service.countDocuments({ isActive: true }),
Contact.countDocuments(),
Contact.find().sort({ createdAt: -1 }).limit(5),
Portfolio.find({ isPublished: true }).sort({ createdAt: -1 }).limit(5)
]);
const stats = {
portfolio: portfolioCount,
services: servicesCount,
contacts: contactsCount,
unreadContacts: await Contact.countDocuments({ isRead: false })
};
res.render('admin/dashboard', {
title: 'Dashboard - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
stats,
recentContacts,
recentPortfolio
});
} catch (error) {
console.error('Dashboard error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading dashboard'
});
}
});
// Portfolio management
router.get('/portfolio', requireAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const [portfolio, total] = await Promise.all([
Portfolio.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Portfolio.countDocuments()
]);
const totalPages = Math.ceil(total / limit);
res.render('admin/portfolio/list', {
title: 'Portfolio Management - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
portfolio,
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Portfolio list error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading portfolio'
});
}
});
// Add portfolio item
router.get('/portfolio/add', requireAuth, (req, res) => {
res.render('admin/portfolio/add', {
title: 'Add Portfolio Item - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
// Edit portfolio item
router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio) {
return res.status(404).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Portfolio item not found'
});
}
res.render('admin/portfolio/edit', {
title: 'Edit Portfolio Item - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
portfolio
});
} catch (error) {
console.error('Portfolio edit error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading portfolio item'
});
}
});
// Services management
router.get('/services', requireAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const [services, total] = await Promise.all([
Service.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Service.countDocuments()
]);
const totalPages = Math.ceil(total / limit);
res.render('admin/services/list', {
title: 'Services Management - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
services,
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Services list error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading services'
});
}
});
// Add service
router.get('/services/add', requireAuth, (req, res) => {
res.render('admin/services/add', {
title: 'Add Service - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
// Edit service
router.get('/services/edit/:id', requireAuth, async (req, res) => {
try {
const service = await Service.findById(req.params.id)
.populate('portfolio', 'title');
if (!service) {
return res.status(404).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Service not found'
});
}
const availablePortfolio = await Portfolio.find({ isPublished: true })
.select('title category');
res.render('admin/services/edit', {
title: 'Edit Service - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
service,
availablePortfolio
});
} catch (error) {
console.error('Service edit error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading service'
});
}
});
// Contacts management
router.get('/contacts', requireAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const status = req.query.status;
let query = {};
if (status && status !== 'all') {
query.status = status;
}
const [contacts, total] = await Promise.all([
Contact.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Contact.countDocuments(query)
]);
const totalPages = Math.ceil(total / limit);
res.render('admin/contacts/list', {
title: 'Contacts Management - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
contacts,
currentStatus: status || 'all',
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Contacts list error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading contacts'
});
}
});
// View contact details
router.get('/contacts/:id', requireAuth, async (req, res) => {
try {
const contact = await Contact.findById(req.params.id);
if (!contact) {
return res.status(404).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Contact not found'
});
}
// Mark as read
if (!contact.isRead) {
contact.isRead = true;
await contact.save();
}
res.render('admin/contacts/view', {
title: 'Contact Details - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
contact
});
} catch (error) {
console.error('Contact view error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading contact'
});
}
});
// Settings
router.get('/settings', requireAuth, async (req, res) => {
try {
const settings = await SiteSettings.findOne() || new SiteSettings();
res.render('admin/settings', {
title: 'Site Settings - Admin Panel',
layout: 'admin/layout',
user: req.session.user,
settings
});
} catch (error) {
console.error('Settings error:', error);
res.status(500).render('admin/error', {
title: 'Error - Admin Panel',
layout: 'admin/layout',
message: 'Error loading settings'
});
}
});
// Media gallery
router.get('/media', requireAuth, (req, res) => {
res.render('admin/media', {
title: 'Media Gallery - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
module.exports = router;

View File

@@ -0,0 +1,201 @@
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
// Login validation rules
const loginValidation = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 })
];
// Login
router.post('/login', loginValidation, async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email, isActive: true });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check password
const isValidPassword = await user.comparePassword(password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Update last login
await user.updateLastLogin();
// Create JWT token
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
// Set session
req.session.user = {
id: user._id,
email: user.email,
name: user.name,
role: user.role
};
res.json({
success: true,
message: 'Login successful',
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
avatar: user.avatar
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Server error'
});
}
});
// Logout
router.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({
success: false,
message: 'Could not log out'
});
}
res.clearCookie('connect.sid');
res.json({
success: true,
message: 'Logout successful'
});
});
});
// Check authentication status
router.get('/me', async (req, res) => {
try {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Not authenticated'
});
}
const user = await User.findById(req.session.user.id)
.select('-password');
if (!user || !user.isActive) {
req.session.destroy();
return res.status(401).json({
success: false,
message: 'User not found or inactive'
});
}
res.json({
success: true,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
avatar: user.avatar,
lastLogin: user.lastLogin
}
});
} catch (error) {
console.error('Auth check error:', error);
res.status(500).json({
success: false,
message: 'Server error'
});
}
});
// Change password
router.put('/change-password', [
body('currentPassword').isLength({ min: 6 }),
body('newPassword').isLength({ min: 6 })
], async (req, res) => {
try {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Not authenticated'
});
}
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const { currentPassword, newPassword } = req.body;
const user = await User.findById(req.session.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Verify current password
const isValidPassword = await user.comparePassword(currentPassword);
if (!isValidPassword) {
return res.status(400).json({
success: false,
message: 'Current password is incorrect'
});
}
// Update password
user.password = newPassword;
await user.save();
res.json({
success: true,
message: 'Password updated successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Server error'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,201 @@
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
// Login validation rules
const loginValidation = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 })
];
// Login
router.post('/login', loginValidation, async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email, isActive: true });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check password
const isValidPassword = await user.comparePassword(password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Update last login
await user.updateLastLogin();
// Create JWT token
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
// Set session
req.session.user = {
id: user._id,
email: user.email,
name: user.name,
role: user.role
};
res.json({
success: true,
message: 'Login successful',
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
avatar: user.avatar
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Server error'
});
}
});
// Logout
router.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({
success: false,
message: 'Could not log out'
});
}
res.clearCookie('connect.sid');
res.json({
success: true,
message: 'Logout successful'
});
});
});
// Check authentication status
router.get('/me', async (req, res) => {
try {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Not authenticated'
});
}
const user = await User.findById(req.session.user.id)
.select('-password');
if (!user || !user.isActive) {
req.session.destroy();
return res.status(401).json({
success: false,
message: 'User not found or inactive'
});
}
res.json({
success: true,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
avatar: user.avatar,
lastLogin: user.lastLogin
}
});
} catch (error) {
console.error('Auth check error:', error);
res.status(500).json({
success: false,
message: 'Server error'
});
}
});
// Change password
router.put('/change-password', [
body('currentPassword').isLength({ min: 6 }),
body('newPassword').isLength({ min: 6 })
], async (req, res) => {
try {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Not authenticated'
});
}
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const { currentPassword, newPassword } = req.body;
const user = await User.findById(req.session.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Verify current password
const isValidPassword = await user.comparePassword(currentPassword);
if (!isValidPassword) {
return res.status(400).json({
success: false,
message: 'Current password is incorrect'
});
}
// Update password
user.password = newPassword;
await user.save();
res.json({
success: true,
message: 'Password updated successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Server error'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,312 @@
const express = require('express');
const router = express.Router();
const Service = require('../models/Service');
// Get all services for calculator
router.get('/services', async (req, res) => {
try {
const services = await Service.find({ isActive: true })
.select('name pricing category features estimatedTime')
.sort({ category: 1, name: 1 });
const servicesByCategory = services.reduce((acc, service) => {
if (!acc[service.category]) {
acc[service.category] = [];
}
acc[service.category].push(service);
return acc;
}, {});
res.json({
success: true,
services: servicesByCategory,
allServices: services
});
} catch (error) {
console.error('Calculator services error:', error);
res.status(500).json({
success: false,
message: 'Error fetching services'
});
}
});
// Calculate project estimate
router.post('/calculate', async (req, res) => {
try {
const {
selectedServices = [],
projectComplexity = 'medium',
timeline = 'standard',
additionalFeatures = [],
customRequirements = ''
} = req.body;
if (!selectedServices.length) {
return res.status(400).json({
success: false,
message: 'Please select at least one service'
});
}
// Get selected services details
const services = await Service.find({
_id: { $in: selectedServices },
isActive: true
});
if (services.length !== selectedServices.length) {
return res.status(400).json({
success: false,
message: 'Some selected services are not available'
});
}
// Calculate base cost
let baseCost = 0;
let totalTime = 0; // in days
services.forEach(service => {
baseCost += service.pricing.basePrice;
totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2;
});
// Apply complexity multiplier
const complexityMultipliers = {
'simple': 0.7,
'medium': 1.0,
'complex': 1.5,
'enterprise': 2.0
};
const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0;
// Apply timeline multiplier
const timelineMultipliers = {
'rush': 1.8, // Less than 2 weeks
'fast': 1.4, // 2-4 weeks
'standard': 1.0, // 1-3 months
'flexible': 0.8 // 3+ months
};
const timelineMultiplier = timelineMultipliers[timeline] || 1.0;
// Additional features cost
const featureCosts = {
'seo-optimization': 300000,
'analytics-setup': 150000,
'social-integration': 200000,
'payment-gateway': 500000,
'multilingual': 400000,
'admin-panel': 600000,
'api-integration': 350000,
'mobile-responsive': 250000,
'ssl-certificate': 100000,
'backup-system': 200000
};
let additionalCost = 0;
additionalFeatures.forEach(feature => {
additionalCost += featureCosts[feature] || 0;
});
// Custom requirements cost (estimated based on description length and complexity)
let customCost = 0;
if (customRequirements && customRequirements.length > 50) {
customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW
}
// Calculate final estimate
const subtotal = baseCost * complexityMultiplier * timelineMultiplier;
const total = subtotal + additionalCost + customCost;
// Calculate time estimate
const timeMultiplier = complexityMultiplier * timelineMultiplier;
const estimatedDays = Math.ceil(totalTime * timeMultiplier);
const estimatedWeeks = Math.ceil(estimatedDays / 7);
// Create price ranges (±20%)
const minPrice = Math.round(total * 0.8);
const maxPrice = Math.round(total * 1.2);
// Breakdown
const breakdown = {
baseServices: Math.round(baseCost),
complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)),
timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)),
additionalFeatures: additionalCost,
customRequirements: customCost,
subtotal: Math.round(subtotal),
total: Math.round(total)
};
const result = {
success: true,
estimate: {
total: Math.round(total),
range: {
min: minPrice,
max: maxPrice,
formatted: `${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}`
},
breakdown,
timeline: {
days: estimatedDays,
weeks: estimatedWeeks,
formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks`
},
currency: 'KRW',
confidence: calculateConfidence(services.length, projectComplexity, customRequirements),
recommendations: generateRecommendations(projectComplexity, timeline, services)
},
selectedServices: services.map(s => ({
id: s._id,
name: s.name,
category: s.category,
basePrice: s.pricing.basePrice
})),
calculatedAt: new Date()
};
res.json(result);
} catch (error) {
console.error('Calculator calculation error:', error);
res.status(500).json({
success: false,
message: 'Error calculating estimate'
});
}
});
// Helper function to calculate confidence level
function calculateConfidence(serviceCount, complexity, customRequirements) {
let confidence = 85; // Base confidence
// Adjust based on service count
if (serviceCount === 1) confidence += 10;
else if (serviceCount > 5) confidence -= 10;
// Adjust based on complexity
if (complexity === 'simple') confidence += 10;
else if (complexity === 'enterprise') confidence -= 15;
// Adjust based on custom requirements
if (customRequirements && customRequirements.length > 200) {
confidence -= 20;
}
return Math.max(60, Math.min(95, confidence));
}
// Helper function to generate recommendations
function generateRecommendations(complexity, timeline, services) {
const recommendations = [];
if (complexity === 'enterprise' && timeline === 'rush') {
recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality');
}
if (services.length > 6) {
recommendations.push('Consider breaking down into phases for better project management');
}
if (timeline === 'flexible') {
recommendations.push('Flexible timeline allows for better cost optimization and quality assurance');
}
const hasDesign = services.some(s => s.category === 'design');
const hasDevelopment = services.some(s => s.category === 'development');
if (hasDevelopment && !hasDesign) {
recommendations.push('Consider adding UI/UX design services for better user experience');
}
return recommendations;
}
// Get pricing guidelines
router.get('/pricing-guide', async (req, res) => {
try {
const pricingGuide = {
serviceCategories: {
'development': {
name: 'Web Development',
priceRange: '₩500,000 - ₩5,000,000',
description: 'Custom web applications and websites'
},
'design': {
name: 'UI/UX Design',
priceRange: '₩300,000 - ₩2,000,000',
description: 'User interface and experience design'
},
'marketing': {
name: 'Digital Marketing',
priceRange: '₩200,000 - ₩1,500,000',
description: 'SEO, social media, and online marketing'
},
'consulting': {
name: 'Technical Consulting',
priceRange: '₩150,000 - ₩1,000,000',
description: 'Technology strategy and consultation'
}
},
complexityFactors: {
'simple': {
name: 'Simple Project',
multiplier: '0.7x',
description: 'Basic functionality, standard design'
},
'medium': {
name: 'Medium Project',
multiplier: '1.0x',
description: 'Moderate complexity, custom features'
},
'complex': {
name: 'Complex Project',
multiplier: '1.5x',
description: 'Advanced features, integrations'
},
'enterprise': {
name: 'Enterprise Project',
multiplier: '2.0x',
description: 'Large scale, high complexity'
}
},
timelineImpact: {
'rush': {
name: 'Rush (< 2 weeks)',
multiplier: '+80%',
description: 'Requires overtime and priority handling'
},
'fast': {
name: 'Fast (2-4 weeks)',
multiplier: '+40%',
description: 'Accelerated timeline'
},
'standard': {
name: 'Standard (1-3 months)',
multiplier: 'Standard',
description: 'Normal project timeline'
},
'flexible': {
name: 'Flexible (3+ months)',
multiplier: '-20%',
description: 'Extended timeline allows optimization'
}
}
};
res.json({
success: true,
pricingGuide
});
} catch (error) {
console.error('Pricing guide error:', error);
res.status(500).json({
success: false,
message: 'Error fetching pricing guide'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,312 @@
const express = require('express');
const router = express.Router();
const Service = require('../models/Service');
// Get all services for calculator
router.get('/services', async (req, res) => {
try {
const services = await Service.find({ isActive: true })
.select('name pricing category features estimatedTime')
.sort({ category: 1, name: 1 });
const servicesByCategory = services.reduce((acc, service) => {
if (!acc[service.category]) {
acc[service.category] = [];
}
acc[service.category].push(service);
return acc;
}, {});
res.json({
success: true,
services: servicesByCategory,
allServices: services
});
} catch (error) {
console.error('Calculator services error:', error);
res.status(500).json({
success: false,
message: 'Error fetching services'
});
}
});
// Calculate project estimate
router.post('/calculate', async (req, res) => {
try {
const {
selectedServices = [],
projectComplexity = 'medium',
timeline = 'standard',
additionalFeatures = [],
customRequirements = ''
} = req.body;
if (!selectedServices.length) {
return res.status(400).json({
success: false,
message: 'Please select at least one service'
});
}
// Get selected services details
const services = await Service.find({
_id: { $in: selectedServices },
isActive: true
});
if (services.length !== selectedServices.length) {
return res.status(400).json({
success: false,
message: 'Some selected services are not available'
});
}
// Calculate base cost
let baseCost = 0;
let totalTime = 0; // in days
services.forEach(service => {
baseCost += service.pricing.basePrice;
totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2;
});
// Apply complexity multiplier
const complexityMultipliers = {
'simple': 0.7,
'medium': 1.0,
'complex': 1.5,
'enterprise': 2.0
};
const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0;
// Apply timeline multiplier
const timelineMultipliers = {
'rush': 1.8, // Less than 2 weeks
'fast': 1.4, // 2-4 weeks
'standard': 1.0, // 1-3 months
'flexible': 0.8 // 3+ months
};
const timelineMultiplier = timelineMultipliers[timeline] || 1.0;
// Additional features cost
const featureCosts = {
'seo-optimization': 300000,
'analytics-setup': 150000,
'social-integration': 200000,
'payment-gateway': 500000,
'multilingual': 400000,
'admin-panel': 600000,
'api-integration': 350000,
'mobile-responsive': 250000,
'ssl-certificate': 100000,
'backup-system': 200000
};
let additionalCost = 0;
additionalFeatures.forEach(feature => {
additionalCost += featureCosts[feature] || 0;
});
// Custom requirements cost (estimated based on description length and complexity)
let customCost = 0;
if (customRequirements && customRequirements.length > 50) {
customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW
}
// Calculate final estimate
const subtotal = baseCost * complexityMultiplier * timelineMultiplier;
const total = subtotal + additionalCost + customCost;
// Calculate time estimate
const timeMultiplier = complexityMultiplier * timelineMultiplier;
const estimatedDays = Math.ceil(totalTime * timeMultiplier);
const estimatedWeeks = Math.ceil(estimatedDays / 7);
// Create price ranges (±20%)
const minPrice = Math.round(total * 0.8);
const maxPrice = Math.round(total * 1.2);
// Breakdown
const breakdown = {
baseServices: Math.round(baseCost),
complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)),
timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)),
additionalFeatures: additionalCost,
customRequirements: customCost,
subtotal: Math.round(subtotal),
total: Math.round(total)
};
const result = {
success: true,
estimate: {
total: Math.round(total),
range: {
min: minPrice,
max: maxPrice,
formatted: `${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}`
},
breakdown,
timeline: {
days: estimatedDays,
weeks: estimatedWeeks,
formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks`
},
currency: 'KRW',
confidence: calculateConfidence(services.length, projectComplexity, customRequirements),
recommendations: generateRecommendations(projectComplexity, timeline, services)
},
selectedServices: services.map(s => ({
id: s._id,
name: s.name,
category: s.category,
basePrice: s.pricing.basePrice
})),
calculatedAt: new Date()
};
res.json(result);
} catch (error) {
console.error('Calculator calculation error:', error);
res.status(500).json({
success: false,
message: 'Error calculating estimate'
});
}
});
// Helper function to calculate confidence level
function calculateConfidence(serviceCount, complexity, customRequirements) {
let confidence = 85; // Base confidence
// Adjust based on service count
if (serviceCount === 1) confidence += 10;
else if (serviceCount > 5) confidence -= 10;
// Adjust based on complexity
if (complexity === 'simple') confidence += 10;
else if (complexity === 'enterprise') confidence -= 15;
// Adjust based on custom requirements
if (customRequirements && customRequirements.length > 200) {
confidence -= 20;
}
return Math.max(60, Math.min(95, confidence));
}
// Helper function to generate recommendations
function generateRecommendations(complexity, timeline, services) {
const recommendations = [];
if (complexity === 'enterprise' && timeline === 'rush') {
recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality');
}
if (services.length > 6) {
recommendations.push('Consider breaking down into phases for better project management');
}
if (timeline === 'flexible') {
recommendations.push('Flexible timeline allows for better cost optimization and quality assurance');
}
const hasDesign = services.some(s => s.category === 'design');
const hasDevelopment = services.some(s => s.category === 'development');
if (hasDevelopment && !hasDesign) {
recommendations.push('Consider adding UI/UX design services for better user experience');
}
return recommendations;
}
// Get pricing guidelines
router.get('/pricing-guide', async (req, res) => {
try {
const pricingGuide = {
serviceCategories: {
'development': {
name: 'Web Development',
priceRange: '₩500,000 - ₩5,000,000',
description: 'Custom web applications and websites'
},
'design': {
name: 'UI/UX Design',
priceRange: '₩300,000 - ₩2,000,000',
description: 'User interface and experience design'
},
'marketing': {
name: 'Digital Marketing',
priceRange: '₩200,000 - ₩1,500,000',
description: 'SEO, social media, and online marketing'
},
'consulting': {
name: 'Technical Consulting',
priceRange: '₩150,000 - ₩1,000,000',
description: 'Technology strategy and consultation'
}
},
complexityFactors: {
'simple': {
name: 'Simple Project',
multiplier: '0.7x',
description: 'Basic functionality, standard design'
},
'medium': {
name: 'Medium Project',
multiplier: '1.0x',
description: 'Moderate complexity, custom features'
},
'complex': {
name: 'Complex Project',
multiplier: '1.5x',
description: 'Advanced features, integrations'
},
'enterprise': {
name: 'Enterprise Project',
multiplier: '2.0x',
description: 'Large scale, high complexity'
}
},
timelineImpact: {
'rush': {
name: 'Rush (< 2 weeks)',
multiplier: '+80%',
description: 'Requires overtime and priority handling'
},
'fast': {
name: 'Fast (2-4 weeks)',
multiplier: '+40%',
description: 'Accelerated timeline'
},
'standard': {
name: 'Standard (1-3 months)',
multiplier: 'Standard',
description: 'Normal project timeline'
},
'flexible': {
name: 'Flexible (3+ months)',
multiplier: '-20%',
description: 'Extended timeline allows optimization'
}
}
};
res.json({
success: true,
pricingGuide
});
} catch (error) {
console.error('Pricing guide error:', error);
res.status(500).json({
success: false,
message: 'Error fetching pricing guide'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,250 @@
const express = require('express');
const router = express.Router();
const { body, validationResult } = require('express-validator');
const nodemailer = require('nodemailer');
const Contact = require('../models/Contact');
const TelegramBot = require('node-telegram-bot-api');
// Initialize Telegram bot if token is provided
let bot = null;
if (process.env.TELEGRAM_BOT_TOKEN) {
bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false });
}
// Contact form validation
const contactValidation = [
body('name').trim().isLength({ min: 2, max: 100 }),
body('email').isEmail().normalizeEmail(),
body('subject').trim().isLength({ min: 5, max: 200 }),
body('message').trim().isLength({ min: 10, max: 2000 }),
body('phone').optional().isMobilePhone(),
body('company').optional().trim().isLength({ max: 100 })
];
// Submit contact form
router.post('/submit', contactValidation, async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const contactData = {
...req.body,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
source: 'website'
};
// Save to database
const contact = new Contact(contactData);
await contact.save();
// Send email notification
await sendEmailNotification(contact);
// Send Telegram notification
await sendTelegramNotification(contact);
res.json({
success: true,
message: 'Your message has been sent successfully. We will get back to you soon!',
contactId: contact._id
});
} catch (error) {
console.error('Contact form error:', error);
res.status(500).json({
success: false,
message: 'Sorry, there was an error sending your message. Please try again.'
});
}
});
// Get project estimate
router.post('/estimate', [
body('services').isArray().notEmpty(),
body('projectType').notEmpty(),
body('timeline').notEmpty(),
body('budget').notEmpty(),
body('description').trim().isLength({ min: 10, max: 1000 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const { services, projectType, timeline, budget, description, contactInfo } = req.body;
// Calculate estimate (simplified)
const estimate = calculateProjectEstimate(services, projectType, timeline);
// Save inquiry to database
const contactData = {
name: contactInfo.name,
email: contactInfo.email,
phone: contactInfo.phone,
company: contactInfo.company,
subject: `Project Estimate Request - ${projectType}`,
message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`,
serviceInterest: projectType,
budget: budget,
timeline: timeline,
source: 'calculator',
ipAddress: req.ip,
userAgent: req.get('User-Agent')
};
const contact = new Contact(contactData);
await contact.save();
// Send notifications
await sendEmailNotification(contact);
await sendTelegramNotification(contact);
res.json({
success: true,
message: 'Your project estimate request has been submitted successfully!',
estimate: estimate,
contactId: contact._id
});
} catch (error) {
console.error('Estimate request error:', error);
res.status(500).json({
success: false,
message: 'Sorry, there was an error processing your request. Please try again.'
});
}
});
// Helper function to send email notification
async function sendEmailNotification(contact) {
if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) {
console.log('Email configuration not provided, skipping email notification');
return;
}
try {
const transporter = nodemailer.createTransporter({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT || 587,
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
const mailOptions = {
from: process.env.EMAIL_USER,
to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER,
subject: `New Contact Form Submission: ${contact.subject}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${contact.name}</p>
<p><strong>Email:</strong> ${contact.email}</p>
<p><strong>Phone:</strong> ${contact.phone || 'Not provided'}</p>
<p><strong>Company:</strong> ${contact.company || 'Not provided'}</p>
<p><strong>Subject:</strong> ${contact.subject}</p>
<p><strong>Message:</strong></p>
<p>${contact.message.replace(/\n/g, '<br>')}</p>
<p><strong>Source:</strong> ${contact.source}</p>
<p><strong>Submitted:</strong> ${contact.createdAt}</p>
<hr>
<p><a href="${process.env.SITE_URL}/admin/contacts/${contact._id}">View in Admin Panel</a></p>
`
};
await transporter.sendMail(mailOptions);
console.log('Email notification sent successfully');
} catch (error) {
console.error('Email notification error:', error);
}
}
// Helper function to send Telegram notification
async function sendTelegramNotification(contact) {
if (!bot || !process.env.TELEGRAM_CHAT_ID) {
console.log('Telegram configuration not provided, skipping Telegram notification');
return;
}
try {
const message = `
🔔 *New Contact Form Submission*
👤 *Name:* ${contact.name}
📧 *Email:* ${contact.email}
📱 *Phone:* ${contact.phone || 'Not provided'}
🏢 *Company:* ${contact.company || 'Not provided'}
📝 *Subject:* ${contact.subject}
💬 *Message:*
${contact.message}
📍 *Source:* ${contact.source}
🕐 *Time:* ${contact.createdAt.toLocaleString()}
[View in Admin Panel](${process.env.SITE_URL}/admin/contacts/${contact._id})
`;
await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, {
parse_mode: 'Markdown',
disable_web_page_preview: true
});
console.log('Telegram notification sent successfully');
} catch (error) {
console.error('Telegram notification error:', error);
}
}
// Helper function to calculate project estimate
function calculateProjectEstimate(services, projectType, timeline) {
const baseRates = {
'web-development': 50000,
'mobile-app': 80000,
'ui-ux-design': 30000,
'branding': 20000,
'e-commerce': 70000,
'consulting': 40000
};
const timelineMultipliers = {
'asap': 1.5,
'1-month': 1.2,
'1-3-months': 1.0,
'3-6-months': 0.9,
'flexible': 0.8
};
let basePrice = baseRates[projectType] || 50000;
let multiplier = timelineMultipliers[timeline] || 1.0;
// Add service modifiers
let serviceModifier = 1.0;
if (services.length > 3) serviceModifier += 0.3;
if (services.length > 5) serviceModifier += 0.5;
const totalEstimate = Math.round(basePrice * multiplier * serviceModifier);
const rangeMin = Math.round(totalEstimate * 0.8);
const rangeMax = Math.round(totalEstimate * 1.3);
return {
base: totalEstimate,
min: rangeMin,
max: rangeMax,
formatted: `${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`,
currency: 'KRW'
};
}
module.exports = router;

View File

@@ -0,0 +1,250 @@
const express = require('express');
const router = express.Router();
const { body, validationResult } = require('express-validator');
const nodemailer = require('nodemailer');
const Contact = require('../models/Contact');
const TelegramBot = require('node-telegram-bot-api');
// Initialize Telegram bot if token is provided
let bot = null;
if (process.env.TELEGRAM_BOT_TOKEN) {
bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false });
}
// Contact form validation
const contactValidation = [
body('name').trim().isLength({ min: 2, max: 100 }),
body('email').isEmail().normalizeEmail(),
body('subject').trim().isLength({ min: 5, max: 200 }),
body('message').trim().isLength({ min: 10, max: 2000 }),
body('phone').optional().isMobilePhone(),
body('company').optional().trim().isLength({ max: 100 })
];
// Submit contact form
router.post('/submit', contactValidation, async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const contactData = {
...req.body,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
source: 'website'
};
// Save to database
const contact = new Contact(contactData);
await contact.save();
// Send email notification
await sendEmailNotification(contact);
// Send Telegram notification
await sendTelegramNotification(contact);
res.json({
success: true,
message: 'Your message has been sent successfully. We will get back to you soon!',
contactId: contact._id
});
} catch (error) {
console.error('Contact form error:', error);
res.status(500).json({
success: false,
message: 'Sorry, there was an error sending your message. Please try again.'
});
}
});
// Get project estimate
router.post('/estimate', [
body('services').isArray().notEmpty(),
body('projectType').notEmpty(),
body('timeline').notEmpty(),
body('budget').notEmpty(),
body('description').trim().isLength({ min: 10, max: 1000 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Invalid input data',
errors: errors.array()
});
}
const { services, projectType, timeline, budget, description, contactInfo } = req.body;
// Calculate estimate (simplified)
const estimate = calculateProjectEstimate(services, projectType, timeline);
// Save inquiry to database
const contactData = {
name: contactInfo.name,
email: contactInfo.email,
phone: contactInfo.phone,
company: contactInfo.company,
subject: `Project Estimate Request - ${projectType}`,
message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`,
serviceInterest: projectType,
budget: budget,
timeline: timeline,
source: 'calculator',
ipAddress: req.ip,
userAgent: req.get('User-Agent')
};
const contact = new Contact(contactData);
await contact.save();
// Send notifications
await sendEmailNotification(contact);
await sendTelegramNotification(contact);
res.json({
success: true,
message: 'Your project estimate request has been submitted successfully!',
estimate: estimate,
contactId: contact._id
});
} catch (error) {
console.error('Estimate request error:', error);
res.status(500).json({
success: false,
message: 'Sorry, there was an error processing your request. Please try again.'
});
}
});
// Helper function to send email notification
async function sendEmailNotification(contact) {
if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) {
console.log('Email configuration not provided, skipping email notification');
return;
}
try {
const transporter = nodemailer.createTransporter({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT || 587,
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
const mailOptions = {
from: process.env.EMAIL_USER,
to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER,
subject: `New Contact Form Submission: ${contact.subject}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${contact.name}</p>
<p><strong>Email:</strong> ${contact.email}</p>
<p><strong>Phone:</strong> ${contact.phone || 'Not provided'}</p>
<p><strong>Company:</strong> ${contact.company || 'Not provided'}</p>
<p><strong>Subject:</strong> ${contact.subject}</p>
<p><strong>Message:</strong></p>
<p>${contact.message.replace(/\n/g, '<br>')}</p>
<p><strong>Source:</strong> ${contact.source}</p>
<p><strong>Submitted:</strong> ${contact.createdAt}</p>
<hr>
<p><a href="${process.env.SITE_URL}/admin/contacts/${contact._id}">View in Admin Panel</a></p>
`
};
await transporter.sendMail(mailOptions);
console.log('Email notification sent successfully');
} catch (error) {
console.error('Email notification error:', error);
}
}
// Helper function to send Telegram notification
async function sendTelegramNotification(contact) {
if (!bot || !process.env.TELEGRAM_CHAT_ID) {
console.log('Telegram configuration not provided, skipping Telegram notification');
return;
}
try {
const message = `
🔔 *New Contact Form Submission*
👤 *Name:* ${contact.name}
📧 *Email:* ${contact.email}
📱 *Phone:* ${contact.phone || 'Not provided'}
🏢 *Company:* ${contact.company || 'Not provided'}
📝 *Subject:* ${contact.subject}
💬 *Message:*
${contact.message}
📍 *Source:* ${contact.source}
🕐 *Time:* ${contact.createdAt.toLocaleString()}
[View in Admin Panel](${process.env.SITE_URL}/admin/contacts/${contact._id})
`;
await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, {
parse_mode: 'Markdown',
disable_web_page_preview: true
});
console.log('Telegram notification sent successfully');
} catch (error) {
console.error('Telegram notification error:', error);
}
}
// Helper function to calculate project estimate
function calculateProjectEstimate(services, projectType, timeline) {
const baseRates = {
'web-development': 50000,
'mobile-app': 80000,
'ui-ux-design': 30000,
'branding': 20000,
'e-commerce': 70000,
'consulting': 40000
};
const timelineMultipliers = {
'asap': 1.5,
'1-month': 1.2,
'1-3-months': 1.0,
'3-6-months': 0.9,
'flexible': 0.8
};
let basePrice = baseRates[projectType] || 50000;
let multiplier = timelineMultipliers[timeline] || 1.0;
// Add service modifiers
let serviceModifier = 1.0;
if (services.length > 3) serviceModifier += 0.3;
if (services.length > 5) serviceModifier += 0.5;
const totalEstimate = Math.round(basePrice * multiplier * serviceModifier);
const rangeMin = Math.round(totalEstimate * 0.8);
const rangeMax = Math.round(totalEstimate * 1.3);
return {
base: totalEstimate,
min: rangeMin,
max: rangeMax,
formatted: `${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`,
currency: 'KRW'
};
}
module.exports = router;

View File

@@ -0,0 +1,203 @@
const express = require('express');
const router = express.Router();
const Portfolio = require('../models/Portfolio');
const Service = require('../models/Service');
const SiteSettings = require('../models/SiteSettings');
// Home page
router.get('/', async (req, res) => {
try {
const [settings, featuredPortfolio, featuredServices] = await Promise.all([
SiteSettings.findOne() || {},
Portfolio.find({ featured: true, isPublished: true })
.sort({ order: 1, createdAt: -1 })
.limit(6),
Service.find({ featured: true, isActive: true })
.sort({ order: 1 })
.limit(4)
]);
res.render('index', {
title: 'SmartSolTech - Innovative Technology Solutions',
settings: settings || {},
featuredPortfolio,
featuredServices,
currentPage: 'home'
});
} catch (error) {
console.error('Home page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// About page
router.get('/about', async (req, res) => {
try {
const settings = await SiteSettings.findOne() || {};
res.render('about', {
title: 'About Us - SmartSolTech',
settings,
currentPage: 'about'
});
} catch (error) {
console.error('About page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Portfolio page
router.get('/portfolio', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 12;
const skip = (page - 1) * limit;
const category = req.query.category;
let query = { isPublished: true };
if (category && category !== 'all') {
query.category = category;
}
const [portfolio, total, categories] = await Promise.all([
Portfolio.find(query)
.sort({ featured: -1, publishedAt: -1 })
.skip(skip)
.limit(limit),
Portfolio.countDocuments(query),
Portfolio.distinct('category', { isPublished: true })
]);
const totalPages = Math.ceil(total / limit);
res.render('portfolio', {
title: 'Portfolio - SmartSolTech',
portfolio,
categories,
currentCategory: category || 'all',
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
},
currentPage: 'portfolio'
});
} catch (error) {
console.error('Portfolio page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Portfolio detail page
router.get('/portfolio/:id', async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio || !portfolio.isPublished) {
return res.status(404).render('404', {
title: '404 - Project Not Found',
message: 'The requested project was not found'
});
}
// Increment view count
portfolio.viewCount += 1;
await portfolio.save();
// Get related projects
const relatedProjects = await Portfolio.find({
_id: { $ne: portfolio._id },
category: portfolio.category,
isPublished: true
}).limit(3);
res.render('portfolio-detail', {
title: `${portfolio.title} - Portfolio - SmartSolTech`,
portfolio,
relatedProjects,
currentPage: 'portfolio'
});
} catch (error) {
console.error('Portfolio detail error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Services page
router.get('/services', async (req, res) => {
try {
const services = await Service.find({ isActive: true })
.sort({ featured: -1, order: 1 })
.populate('portfolio', 'title images');
const categories = await Service.distinct('category', { isActive: true });
res.render('services', {
title: 'Services - SmartSolTech',
services,
categories,
currentPage: 'services'
});
} catch (error) {
console.error('Services page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Calculator page
router.get('/calculator', async (req, res) => {
try {
const services = await Service.find({ isActive: true })
.select('name pricing category')
.sort({ category: 1, name: 1 });
res.render('calculator', {
title: 'Project Calculator - SmartSolTech',
services,
currentPage: 'calculator'
});
} catch (error) {
console.error('Calculator page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Contact page
router.get('/contact', async (req, res) => {
try {
const settings = await SiteSettings.findOne() || {};
res.render('contact', {
title: 'Contact Us - SmartSolTech',
settings,
currentPage: 'contact'
});
} catch (error) {
console.error('Contact page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,203 @@
const express = require('express');
const router = express.Router();
const Portfolio = require('../models/Portfolio');
const Service = require('../models/Service');
const SiteSettings = require('../models/SiteSettings');
// Home page
router.get('/', async (req, res) => {
try {
const [settings, featuredPortfolio, featuredServices] = await Promise.all([
SiteSettings.findOne() || {},
Portfolio.find({ featured: true, isPublished: true })
.sort({ order: 1, createdAt: -1 })
.limit(6),
Service.find({ featured: true, isActive: true })
.sort({ order: 1 })
.limit(4)
]);
res.render('index', {
title: 'SmartSolTech - Innovative Technology Solutions',
settings: settings || {},
featuredPortfolio,
featuredServices,
currentPage: 'home'
});
} catch (error) {
console.error('Home page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// About page
router.get('/about', async (req, res) => {
try {
const settings = await SiteSettings.findOne() || {};
res.render('about', {
title: 'About Us - SmartSolTech',
settings,
currentPage: 'about'
});
} catch (error) {
console.error('About page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Portfolio page
router.get('/portfolio', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = 12;
const skip = (page - 1) * limit;
const category = req.query.category;
let query = { isPublished: true };
if (category && category !== 'all') {
query.category = category;
}
const [portfolio, total, categories] = await Promise.all([
Portfolio.find(query)
.sort({ featured: -1, publishedAt: -1 })
.skip(skip)
.limit(limit),
Portfolio.countDocuments(query),
Portfolio.distinct('category', { isPublished: true })
]);
const totalPages = Math.ceil(total / limit);
res.render('portfolio', {
title: 'Portfolio - SmartSolTech',
portfolio,
categories,
currentCategory: category || 'all',
pagination: {
current: page,
total: totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
},
currentPage: 'portfolio'
});
} catch (error) {
console.error('Portfolio page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Portfolio detail page
router.get('/portfolio/:id', async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio || !portfolio.isPublished) {
return res.status(404).render('404', {
title: '404 - Project Not Found',
message: 'The requested project was not found'
});
}
// Increment view count
portfolio.viewCount += 1;
await portfolio.save();
// Get related projects
const relatedProjects = await Portfolio.find({
_id: { $ne: portfolio._id },
category: portfolio.category,
isPublished: true
}).limit(3);
res.render('portfolio-detail', {
title: `${portfolio.title} - Portfolio - SmartSolTech`,
portfolio,
relatedProjects,
currentPage: 'portfolio'
});
} catch (error) {
console.error('Portfolio detail error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Services page
router.get('/services', async (req, res) => {
try {
const services = await Service.find({ isActive: true })
.sort({ featured: -1, order: 1 })
.populate('portfolio', 'title images');
const categories = await Service.distinct('category', { isActive: true });
res.render('services', {
title: 'Services - SmartSolTech',
services,
categories,
currentPage: 'services'
});
} catch (error) {
console.error('Services page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Calculator page
router.get('/calculator', async (req, res) => {
try {
const services = await Service.find({ isActive: true })
.select('name pricing category')
.sort({ category: 1, name: 1 });
res.render('calculator', {
title: 'Project Calculator - SmartSolTech',
services,
currentPage: 'calculator'
});
} catch (error) {
console.error('Calculator page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
// Contact page
router.get('/contact', async (req, res) => {
try {
const settings = await SiteSettings.findOne() || {};
res.render('contact', {
title: 'Contact Us - SmartSolTech',
settings,
currentPage: 'contact'
});
} catch (error) {
console.error('Contact page error:', error);
res.status(500).render('error', {
title: 'Error',
message: 'Something went wrong'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,345 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs').promises;
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
next();
};
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const uploadPath = path.join(__dirname, '../public/uploads');
try {
await fs.mkdir(uploadPath, { recursive: true });
cb(null, uploadPath);
} catch (error) {
cb(error);
}
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB
files: 10
},
fileFilter: (req, file, cb) => {
// Allow images only
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
}
});
// Upload single image
router.post('/upload', requireAuth, upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
const originalPath = req.file.path;
const filename = req.file.filename;
const nameWithoutExt = path.parse(filename).name;
// Create optimized versions
const sizes = {
thumbnail: { width: 300, height: 200 },
medium: { width: 800, height: 600 },
large: { width: 1200, height: 900 }
};
const optimizedImages = {};
for (const [sizeName, dimensions] of Object.entries(sizes)) {
const outputPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-${sizeName}.webp`
);
await sharp(originalPath)
.resize(dimensions.width, dimensions.height, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 85 })
.toFile(outputPath);
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
}
// Keep original as well (converted to webp for better compression)
const originalWebpPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-original.webp`
);
await sharp(originalPath)
.webp({ quality: 90 })
.toFile(originalWebpPath);
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
// Remove the original uploaded file
await fs.unlink(originalPath);
res.json({
success: true,
message: 'Image uploaded and optimized successfully',
images: optimizedImages,
metadata: {
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size
}
});
} catch (error) {
console.error('Image upload error:', error);
// Clean up files on error
if (req.file && req.file.path) {
try {
await fs.unlink(req.file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
res.status(500).json({
success: false,
message: 'Error uploading image'
});
}
});
// Upload multiple images
router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded'
});
}
const uploadedImages = [];
for (const file of req.files) {
try {
const originalPath = file.path;
const filename = file.filename;
const nameWithoutExt = path.parse(filename).name;
// Create optimized versions
const sizes = {
thumbnail: { width: 300, height: 200 },
medium: { width: 800, height: 600 },
large: { width: 1200, height: 900 }
};
const optimizedImages = {};
for (const [sizeName, dimensions] of Object.entries(sizes)) {
const outputPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-${sizeName}.webp`
);
await sharp(originalPath)
.resize(dimensions.width, dimensions.height, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 85 })
.toFile(outputPath);
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
}
// Original as webp
const originalWebpPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-original.webp`
);
await sharp(originalPath)
.webp({ quality: 90 })
.toFile(originalWebpPath);
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
uploadedImages.push({
originalName: file.originalname,
images: optimizedImages,
metadata: {
mimeType: file.mimetype,
size: file.size
}
});
// Remove original file
await fs.unlink(originalPath);
} catch (fileError) {
console.error(`Error processing file ${file.originalname}:`, fileError);
// Clean up this file and continue with others
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
}
res.json({
success: true,
message: `${uploadedImages.length} images uploaded and optimized successfully`,
images: uploadedImages
});
} catch (error) {
console.error('Multiple images upload error:', error);
// Clean up files on error
if (req.files) {
for (const file of req.files) {
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
}
res.status(500).json({
success: false,
message: 'Error uploading images'
});
}
});
// Delete image
router.delete('/:filename', requireAuth, async (req, res) => {
try {
const filename = req.params.filename;
const uploadPath = path.join(__dirname, '../public/uploads');
// Security check - ensure filename doesn't contain path traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return res.status(400).json({
success: false,
message: 'Invalid filename'
});
}
const filePath = path.join(uploadPath, filename);
try {
await fs.access(filePath);
await fs.unlink(filePath);
res.json({
success: true,
message: 'Image deleted successfully'
});
} catch (error) {
if (error.code === 'ENOENT') {
return res.status(404).json({
success: false,
message: 'Image not found'
});
}
throw error;
}
} catch (error) {
console.error('Image deletion error:', error);
res.status(500).json({
success: false,
message: 'Error deleting image'
});
}
});
// List uploaded images
router.get('/list', requireAuth, async (req, res) => {
try {
const uploadPath = path.join(__dirname, '../public/uploads');
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const files = await fs.readdir(uploadPath);
const imageFiles = files.filter(file =>
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
);
const total = imageFiles.length;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedFiles = imageFiles.slice(start, end);
const imagesWithStats = await Promise.all(
paginatedFiles.map(async (file) => {
try {
const filePath = path.join(uploadPath, file);
const stats = await fs.stat(filePath);
return {
filename: file,
url: `/uploads/${file}`,
size: stats.size,
modified: stats.mtime,
isImage: true
};
} catch (error) {
console.error(`Error getting stats for ${file}:`, error);
return null;
}
})
);
const validImages = imagesWithStats.filter(img => img !== null);
res.json({
success: true,
images: validImages,
pagination: {
current: page,
total: totalPages,
limit,
totalItems: total,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('List images error:', error);
res.status(500).json({
success: false,
message: 'Error listing images'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,345 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs').promises;
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
next();
};
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const uploadPath = path.join(__dirname, '../public/uploads');
try {
await fs.mkdir(uploadPath, { recursive: true });
cb(null, uploadPath);
} catch (error) {
cb(error);
}
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB
files: 10
},
fileFilter: (req, file, cb) => {
// Allow images only
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
}
});
// Upload single image
router.post('/upload', requireAuth, upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
const originalPath = req.file.path;
const filename = req.file.filename;
const nameWithoutExt = path.parse(filename).name;
// Create optimized versions
const sizes = {
thumbnail: { width: 300, height: 200 },
medium: { width: 800, height: 600 },
large: { width: 1200, height: 900 }
};
const optimizedImages = {};
for (const [sizeName, dimensions] of Object.entries(sizes)) {
const outputPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-${sizeName}.webp`
);
await sharp(originalPath)
.resize(dimensions.width, dimensions.height, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 85 })
.toFile(outputPath);
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
}
// Keep original as well (converted to webp for better compression)
const originalWebpPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-original.webp`
);
await sharp(originalPath)
.webp({ quality: 90 })
.toFile(originalWebpPath);
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
// Remove the original uploaded file
await fs.unlink(originalPath);
res.json({
success: true,
message: 'Image uploaded and optimized successfully',
images: optimizedImages,
metadata: {
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size
}
});
} catch (error) {
console.error('Image upload error:', error);
// Clean up files on error
if (req.file && req.file.path) {
try {
await fs.unlink(req.file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
res.status(500).json({
success: false,
message: 'Error uploading image'
});
}
});
// Upload multiple images
router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded'
});
}
const uploadedImages = [];
for (const file of req.files) {
try {
const originalPath = file.path;
const filename = file.filename;
const nameWithoutExt = path.parse(filename).name;
// Create optimized versions
const sizes = {
thumbnail: { width: 300, height: 200 },
medium: { width: 800, height: 600 },
large: { width: 1200, height: 900 }
};
const optimizedImages = {};
for (const [sizeName, dimensions] of Object.entries(sizes)) {
const outputPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-${sizeName}.webp`
);
await sharp(originalPath)
.resize(dimensions.width, dimensions.height, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 85 })
.toFile(outputPath);
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
}
// Original as webp
const originalWebpPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-original.webp`
);
await sharp(originalPath)
.webp({ quality: 90 })
.toFile(originalWebpPath);
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
uploadedImages.push({
originalName: file.originalname,
images: optimizedImages,
metadata: {
mimeType: file.mimetype,
size: file.size
}
});
// Remove original file
await fs.unlink(originalPath);
} catch (fileError) {
console.error(`Error processing file ${file.originalname}:`, fileError);
// Clean up this file and continue with others
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
}
res.json({
success: true,
message: `${uploadedImages.length} images uploaded and optimized successfully`,
images: uploadedImages
});
} catch (error) {
console.error('Multiple images upload error:', error);
// Clean up files on error
if (req.files) {
for (const file of req.files) {
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
}
res.status(500).json({
success: false,
message: 'Error uploading images'
});
}
});
// Delete image
router.delete('/:filename', requireAuth, async (req, res) => {
try {
const filename = req.params.filename;
const uploadPath = path.join(__dirname, '../public/uploads');
// Security check - ensure filename doesn't contain path traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return res.status(400).json({
success: false,
message: 'Invalid filename'
});
}
const filePath = path.join(uploadPath, filename);
try {
await fs.access(filePath);
await fs.unlink(filePath);
res.json({
success: true,
message: 'Image deleted successfully'
});
} catch (error) {
if (error.code === 'ENOENT') {
return res.status(404).json({
success: false,
message: 'Image not found'
});
}
throw error;
}
} catch (error) {
console.error('Image deletion error:', error);
res.status(500).json({
success: false,
message: 'Error deleting image'
});
}
});
// List uploaded images
router.get('/list', requireAuth, async (req, res) => {
try {
const uploadPath = path.join(__dirname, '../public/uploads');
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const files = await fs.readdir(uploadPath);
const imageFiles = files.filter(file =>
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
);
const total = imageFiles.length;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedFiles = imageFiles.slice(start, end);
const imagesWithStats = await Promise.all(
paginatedFiles.map(async (file) => {
try {
const filePath = path.join(uploadPath, file);
const stats = await fs.stat(filePath);
return {
filename: file,
url: `/uploads/${file}`,
size: stats.size,
modified: stats.mtime,
isImage: true
};
} catch (error) {
console.error(`Error getting stats for ${file}:`, error);
return null;
}
})
);
const validImages = imagesWithStats.filter(img => img !== null);
res.json({
success: true,
images: validImages,
pagination: {
current: page,
total: totalPages,
limit,
totalItems: total,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('List images error:', error);
res.status(500).json({
success: false,
message: 'Error listing images'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const router = express.Router();
const Portfolio = require('../models/Portfolio');
// Get all portfolio items
router.get('/', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 12;
const skip = (page - 1) * limit;
const category = req.query.category;
const search = req.query.search;
const featured = req.query.featured;
// Build query
let query = { isPublished: true };
if (category && category !== 'all') {
query.category = category;
}
if (featured === 'true') {
query.featured = true;
}
if (search) {
query.$text = { $search: search };
}
// Get portfolio items
const [portfolio, total] = await Promise.all([
Portfolio.find(query)
.sort({ featured: -1, publishedAt: -1 })
.skip(skip)
.limit(limit)
.select('title shortDescription category technologies images status publishedAt viewCount'),
Portfolio.countDocuments(query)
]);
const totalPages = Math.ceil(total / limit);
res.json({
success: true,
portfolio,
pagination: {
current: page,
total: totalPages,
limit,
totalItems: total,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Portfolio API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching portfolio'
});
}
});
// Get single portfolio item
router.get('/:id', async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio || !portfolio.isPublished) {
return res.status(404).json({
success: false,
message: 'Portfolio item not found'
});
}
// Increment view count
portfolio.viewCount += 1;
await portfolio.save();
// Get related projects
const relatedProjects = await Portfolio.find({
_id: { $ne: portfolio._id },
category: portfolio.category,
isPublished: true
})
.select('title shortDescription images')
.limit(4);
res.json({
success: true,
portfolio,
relatedProjects
});
} catch (error) {
console.error('Portfolio detail API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching portfolio item'
});
}
});
// Get portfolio categories
router.get('/meta/categories', async (req, res) => {
try {
const categories = await Portfolio.distinct('category', { isPublished: true });
// Get count for each category
const categoriesWithCount = await Promise.all(
categories.map(async (category) => {
const count = await Portfolio.countDocuments({
category,
isPublished: true
});
return { category, count };
})
);
res.json({
success: true,
categories: categoriesWithCount
});
} catch (error) {
console.error('Portfolio categories API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching categories'
});
}
});
// Like portfolio item
router.post('/:id/like', async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio || !portfolio.isPublished) {
return res.status(404).json({
success: false,
message: 'Portfolio item not found'
});
}
portfolio.likes += 1;
await portfolio.save();
res.json({
success: true,
likes: portfolio.likes
});
} catch (error) {
console.error('Portfolio like API error:', error);
res.status(500).json({
success: false,
message: 'Error liking portfolio item'
});
}
});
// Search portfolio
router.get('/search/:term', async (req, res) => {
try {
const searchTerm = req.params.term;
const limit = parseInt(req.query.limit) || 10;
const portfolio = await Portfolio.find({
$and: [
{ isPublished: true },
{
$or: [
{ title: { $regex: searchTerm, $options: 'i' } },
{ description: { $regex: searchTerm, $options: 'i' } },
{ technologies: { $in: [new RegExp(searchTerm, 'i')] } }
]
}
]
})
.select('title shortDescription category images')
.sort({ featured: -1, publishedAt: -1 })
.limit(limit);
res.json({
success: true,
portfolio,
searchTerm,
count: portfolio.length
});
} catch (error) {
console.error('Portfolio search API error:', error);
res.status(500).json({
success: false,
message: 'Error searching portfolio'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const router = express.Router();
const Portfolio = require('../models/Portfolio');
// Get all portfolio items
router.get('/', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 12;
const skip = (page - 1) * limit;
const category = req.query.category;
const search = req.query.search;
const featured = req.query.featured;
// Build query
let query = { isPublished: true };
if (category && category !== 'all') {
query.category = category;
}
if (featured === 'true') {
query.featured = true;
}
if (search) {
query.$text = { $search: search };
}
// Get portfolio items
const [portfolio, total] = await Promise.all([
Portfolio.find(query)
.sort({ featured: -1, publishedAt: -1 })
.skip(skip)
.limit(limit)
.select('title shortDescription category technologies images status publishedAt viewCount'),
Portfolio.countDocuments(query)
]);
const totalPages = Math.ceil(total / limit);
res.json({
success: true,
portfolio,
pagination: {
current: page,
total: totalPages,
limit,
totalItems: total,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('Portfolio API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching portfolio'
});
}
});
// Get single portfolio item
router.get('/:id', async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio || !portfolio.isPublished) {
return res.status(404).json({
success: false,
message: 'Portfolio item not found'
});
}
// Increment view count
portfolio.viewCount += 1;
await portfolio.save();
// Get related projects
const relatedProjects = await Portfolio.find({
_id: { $ne: portfolio._id },
category: portfolio.category,
isPublished: true
})
.select('title shortDescription images')
.limit(4);
res.json({
success: true,
portfolio,
relatedProjects
});
} catch (error) {
console.error('Portfolio detail API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching portfolio item'
});
}
});
// Get portfolio categories
router.get('/meta/categories', async (req, res) => {
try {
const categories = await Portfolio.distinct('category', { isPublished: true });
// Get count for each category
const categoriesWithCount = await Promise.all(
categories.map(async (category) => {
const count = await Portfolio.countDocuments({
category,
isPublished: true
});
return { category, count };
})
);
res.json({
success: true,
categories: categoriesWithCount
});
} catch (error) {
console.error('Portfolio categories API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching categories'
});
}
});
// Like portfolio item
router.post('/:id/like', async (req, res) => {
try {
const portfolio = await Portfolio.findById(req.params.id);
if (!portfolio || !portfolio.isPublished) {
return res.status(404).json({
success: false,
message: 'Portfolio item not found'
});
}
portfolio.likes += 1;
await portfolio.save();
res.json({
success: true,
likes: portfolio.likes
});
} catch (error) {
console.error('Portfolio like API error:', error);
res.status(500).json({
success: false,
message: 'Error liking portfolio item'
});
}
});
// Search portfolio
router.get('/search/:term', async (req, res) => {
try {
const searchTerm = req.params.term;
const limit = parseInt(req.query.limit) || 10;
const portfolio = await Portfolio.find({
$and: [
{ isPublished: true },
{
$or: [
{ title: { $regex: searchTerm, $options: 'i' } },
{ description: { $regex: searchTerm, $options: 'i' } },
{ technologies: { $in: [new RegExp(searchTerm, 'i')] } }
]
}
]
})
.select('title shortDescription category images')
.sort({ featured: -1, publishedAt: -1 })
.limit(limit);
res.json({
success: true,
portfolio,
searchTerm,
count: portfolio.length
});
} catch (error) {
console.error('Portfolio search API error:', error);
res.status(500).json({
success: false,
message: 'Error searching portfolio'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,141 @@
const express = require('express');
const router = express.Router();
const Service = require('../models/Service');
const Portfolio = require('../models/Portfolio');
// Get all services
router.get('/', async (req, res) => {
try {
const category = req.query.category;
const featured = req.query.featured;
let query = { isActive: true };
if (category && category !== 'all') {
query.category = category;
}
if (featured === 'true') {
query.featured = true;
}
const services = await Service.find(query)
.populate('portfolio', 'title images')
.sort({ featured: -1, order: 1 });
res.json({
success: true,
services
});
} catch (error) {
console.error('Services API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching services'
});
}
});
// Get single service
router.get('/:id', async (req, res) => {
try {
const service = await Service.findById(req.params.id)
.populate('portfolio', 'title shortDescription images category');
if (!service || !service.isActive) {
return res.status(404).json({
success: false,
message: 'Service not found'
});
}
// Get related services
const relatedServices = await Service.find({
_id: { $ne: service._id },
category: service.category,
isActive: true
})
.select('name shortDescription icon pricing')
.limit(3);
res.json({
success: true,
service,
relatedServices
});
} catch (error) {
console.error('Service detail API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching service'
});
}
});
// Get service categories
router.get('/meta/categories', async (req, res) => {
try {
const categories = await Service.distinct('category', { isActive: true });
// Get count for each category
const categoriesWithCount = await Promise.all(
categories.map(async (category) => {
const count = await Service.countDocuments({
category,
isActive: true
});
return { category, count };
})
);
res.json({
success: true,
categories: categoriesWithCount
});
} catch (error) {
console.error('Service categories API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching categories'
});
}
});
// Search services
router.get('/search/:term', async (req, res) => {
try {
const searchTerm = req.params.term;
const limit = parseInt(req.query.limit) || 10;
const services = await Service.find({
$and: [
{ isActive: true },
{
$or: [
{ name: { $regex: searchTerm, $options: 'i' } },
{ description: { $regex: searchTerm, $options: 'i' } },
{ tags: { $in: [new RegExp(searchTerm, 'i')] } }
]
}
]
})
.select('name shortDescription icon pricing category')
.sort({ featured: -1, order: 1 })
.limit(limit);
res.json({
success: true,
services,
searchTerm,
count: services.length
});
} catch (error) {
console.error('Service search API error:', error);
res.status(500).json({
success: false,
message: 'Error searching services'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,141 @@
const express = require('express');
const router = express.Router();
const Service = require('../models/Service');
const Portfolio = require('../models/Portfolio');
// Get all services
router.get('/', async (req, res) => {
try {
const category = req.query.category;
const featured = req.query.featured;
let query = { isActive: true };
if (category && category !== 'all') {
query.category = category;
}
if (featured === 'true') {
query.featured = true;
}
const services = await Service.find(query)
.populate('portfolio', 'title images')
.sort({ featured: -1, order: 1 });
res.json({
success: true,
services
});
} catch (error) {
console.error('Services API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching services'
});
}
});
// Get single service
router.get('/:id', async (req, res) => {
try {
const service = await Service.findById(req.params.id)
.populate('portfolio', 'title shortDescription images category');
if (!service || !service.isActive) {
return res.status(404).json({
success: false,
message: 'Service not found'
});
}
// Get related services
const relatedServices = await Service.find({
_id: { $ne: service._id },
category: service.category,
isActive: true
})
.select('name shortDescription icon pricing')
.limit(3);
res.json({
success: true,
service,
relatedServices
});
} catch (error) {
console.error('Service detail API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching service'
});
}
});
// Get service categories
router.get('/meta/categories', async (req, res) => {
try {
const categories = await Service.distinct('category', { isActive: true });
// Get count for each category
const categoriesWithCount = await Promise.all(
categories.map(async (category) => {
const count = await Service.countDocuments({
category,
isActive: true
});
return { category, count };
})
);
res.json({
success: true,
categories: categoriesWithCount
});
} catch (error) {
console.error('Service categories API error:', error);
res.status(500).json({
success: false,
message: 'Error fetching categories'
});
}
});
// Search services
router.get('/search/:term', async (req, res) => {
try {
const searchTerm = req.params.term;
const limit = parseInt(req.query.limit) || 10;
const services = await Service.find({
$and: [
{ isActive: true },
{
$or: [
{ name: { $regex: searchTerm, $options: 'i' } },
{ description: { $regex: searchTerm, $options: 'i' } },
{ tags: { $in: [new RegExp(searchTerm, 'i')] } }
]
}
]
})
.select('name shortDescription icon pricing category')
.sort({ featured: -1, order: 1 })
.limit(limit);
res.json({
success: true,
services,
searchTerm,
count: services.length
});
} catch (error) {
console.error('Service search API error:', error);
res.status(500).json({
success: false,
message: 'Error searching services'
});
}
});
module.exports = router;