From b0361594b15155f7e0249cdbcb5020faf6c0142d Mon Sep 17 00:00:00 2001 From: Noro Studio Date: Tue, 30 Jun 2026 00:51:32 +0330 Subject: [PATCH] Initial commit: API and deploy configs --- api/.env.example | 7 + api/.gitignore | 4 + api/package.json | 20 +++ api/src/auth.js | 35 +++++ api/src/db.js | 46 ++++++ api/src/index.js | 239 +++++++++++++++++++++++++++++ api/src/seed.js | 123 +++++++++++++++ api/src/services.seed.json | 260 ++++++++++++++++++++++++++++++++ deploy/.gitignore | 1 + deploy/deploy.sh | 29 ++++ deploy/env/nuxt.env.example | 2 + deploy/nginx/studionoro.conf | 84 +++++++++++ deploy/pm2/ecosystem.config.cjs | 31 ++++ deploy/setup-vps.sh | 90 +++++++++++ deploy/upload-notes.txt | 14 ++ 15 files changed, 985 insertions(+) create mode 100644 api/.env.example create mode 100644 api/.gitignore create mode 100644 api/package.json create mode 100644 api/src/auth.js create mode 100644 api/src/db.js create mode 100644 api/src/index.js create mode 100644 api/src/seed.js create mode 100644 api/src/services.seed.json create mode 100644 deploy/.gitignore create mode 100644 deploy/deploy.sh create mode 100644 deploy/env/nuxt.env.example create mode 100644 deploy/nginx/studionoro.conf create mode 100644 deploy/pm2/ecosystem.config.cjs create mode 100644 deploy/setup-vps.sh create mode 100644 deploy/upload-notes.txt diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..2265462 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,7 @@ +PORT=4000 +NODE_ENV=production +JWT_SECRET=change-this-to-a-long-random-secret +ADMIN_EMAIL=admin@studionoro.ir +ADMIN_PASSWORD=change-this-strong-password +API_PUBLIC_URL=https://api.studionoro.ir +CORS_ORIGIN=https://studionoro.ir,https://www.studionoro.ir,https://admin.studionoro.ir diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..b0f7707 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +data/ +uploads/ diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..f86cea3 --- /dev/null +++ b/api/package.json @@ -0,0 +1,20 @@ +{ + "name": "studionoro-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "seed": "node src/seed.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "uuid": "^10.0.0" + } +} diff --git a/api/src/auth.js b/api/src/auth.js new file mode 100644 index 0000000..77fd9ad --- /dev/null +++ b/api/src/auth.js @@ -0,0 +1,35 @@ +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import { readDb } from './db.js' + +const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret' + +export function authMiddleware(req, res, next) { + const header = req.headers.authorization + if (!header?.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized' }) + } + try { + req.user = jwt.verify(header.slice(7), JWT_SECRET) + next() + } catch { + return res.status(401).json({ error: 'Invalid token' }) + } +} + +export function loginHandler(req, res) { + const { email, password } = req.body || {} + const adminEmail = process.env.ADMIN_EMAIL || 'admin@norostudio.ir' + const adminPassword = process.env.ADMIN_PASSWORD || 'admin123' + + if (email !== adminEmail || password !== adminPassword) { + return res.status(401).json({ error: 'ایمیل یا رمز عبور نادرست است' }) + } + + const token = jwt.sign({ email, role: 'admin' }, JWT_SECRET, { expiresIn: '7d' }) + res.json({ token, user: { email, role: 'admin' } }) +} + +export function meHandler(req, res) { + res.json({ user: req.user }) +} diff --git a/api/src/db.js b/api/src/db.js new file mode 100644 index 0000000..026d623 --- /dev/null +++ b/api/src/db.js @@ -0,0 +1,46 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const DATA_DIR = join(__dirname, '..', 'data') +const DB_PATH = join(DATA_DIR, 'content.json') + +const defaultDb = () => ({ + site: {}, + navigation: [], + portfolioCategories: [], + projects: [], + services: [], + serviceProjectCategories: {}, + clients: [], + stats: [], + blogPosts: [], + media: [] +}) + +function ensureDataDir() { + if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }) +} + +export function readDb() { + ensureDataDir() + if (!existsSync(DB_PATH)) { + const db = defaultDb() + writeDb(db) + return db + } + return JSON.parse(readFileSync(DB_PATH, 'utf-8')) +} + +export function writeDb(db) { + ensureDataDir() + writeFileSync(DB_PATH, JSON.stringify(db, null, 2), 'utf-8') +} + +export function updateDb(mutator) { + const db = readDb() + mutator(db) + writeDb(db) + return db +} diff --git a/api/src/index.js b/api/src/index.js new file mode 100644 index 0000000..3b3b448 --- /dev/null +++ b/api/src/index.js @@ -0,0 +1,239 @@ +import 'dotenv/config' +import express from 'express' +import cors from 'cors' +import multer from 'multer' +import { existsSync, mkdirSync } from 'node:fs' +import { dirname, extname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { randomUUID } from 'node:crypto' +import { authMiddleware, loginHandler, meHandler } from './auth.js' +import { readDb, updateDb } from './db.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const UPLOADS_DIR = join(__dirname, '..', 'uploads') +const PORT = Number(process.env.PORT) || 4000 + +if (!existsSync(UPLOADS_DIR)) mkdirSync(UPLOADS_DIR, { recursive: true }) + +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, UPLOADS_DIR), + filename: (_req, file, cb) => { + const ext = extname(file.originalname) + cb(null, `${randomUUID()}${ext}`) + }, +}) +const upload = multer({ storage }) + +const app = express() +const corsOrigin = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()) || [ + 'http://localhost:5173', + 'http://localhost:3000', +] + +app.use(cors({ origin: corsOrigin, credentials: true })) +app.use(express.json({ limit: '10mb' })) +app.use('/uploads', express.static(UPLOADS_DIR)) + +app.get('/health', (_req, res) => { + res.json({ ok: true, service: 'studionoro-api' }) +}) + +app.post('/api/auth/login', loginHandler) +app.get('/api/auth/me', authMiddleware, meHandler) + +app.get('/api/site', (_req, res) => { + res.json(readDb().site) +}) + +app.put('/api/site', authMiddleware, (req, res) => { + const site = updateDb((db) => { + db.site = req.body + }).site + res.json(site) +}) + +app.get('/api/navigation', (_req, res) => { + res.json(readDb().navigation) +}) + +app.put('/api/navigation', authMiddleware, (req, res) => { + const navigation = updateDb((db) => { + db.navigation = req.body + }).navigation + res.json(navigation) +}) + +app.get('/api/portfolio/categories', (_req, res) => { + res.json(readDb().portfolioCategories) +}) + +app.get('/api/portfolio/projects', (_req, res) => { + res.json(readDb().projects) +}) + +app.get('/api/portfolio/projects/:id', (req, res) => { + const project = readDb().projects.find((p) => p.id === req.params.id) + if (!project) return res.status(404).json({ error: 'پروژه یافت نشد' }) + res.json(project) +}) + +app.post('/api/portfolio/projects', authMiddleware, (req, res) => { + const project = { id: randomUUID(), ...req.body } + updateDb((db) => { + db.projects.push(project) + }) + res.status(201).json(project) +}) + +app.put('/api/portfolio/projects/:id', authMiddleware, (req, res) => { + let updated = null + updateDb((db) => { + const i = db.projects.findIndex((p) => p.id === req.params.id) + if (i === -1) return + db.projects[i] = { ...db.projects[i], ...req.body, id: req.params.id } + updated = db.projects[i] + }) + if (!updated) return res.status(404).json({ error: 'پروژه یافت نشد' }) + res.json(updated) +}) + +app.delete('/api/portfolio/projects/:id', authMiddleware, (req, res) => { + let found = false + updateDb((db) => { + const i = db.projects.findIndex((p) => p.id === req.params.id) + if (i !== -1) { + db.projects.splice(i, 1) + found = true + } + }) + if (!found) return res.status(404).json({ error: 'پروژه یافت نشد' }) + res.status(204).end() +}) + +app.get('/api/services', (_req, res) => { + res.json(readDb().services) +}) + +app.put('/api/services', authMiddleware, (req, res) => { + const services = updateDb((db) => { + db.services = req.body + }).services + res.json(services) +}) + +app.get('/api/services/:id', (req, res) => { + const service = readDb().services.find((s) => s.id === req.params.id) + if (!service) return res.status(404).json({ error: 'خدمت یافت نشد' }) + res.json(service) +}) + +app.put('/api/services/:id', authMiddleware, (req, res) => { + let updated = null + updateDb((db) => { + const i = db.services.findIndex((s) => s.id === req.params.id) + if (i === -1) return + db.services[i] = { ...db.services[i], ...req.body, id: req.params.id } + updated = db.services[i] + }) + if (!updated) return res.status(404).json({ error: 'خدمت یافت نشد' }) + res.json(updated) +}) + +app.get('/api/clients', (_req, res) => { + res.json(readDb().clients) +}) + +app.put('/api/clients', authMiddleware, (req, res) => { + const clients = updateDb((db) => { + db.clients = req.body + }).clients + res.json(clients) +}) + +app.get('/api/stats', (_req, res) => { + res.json(readDb().stats || []) +}) + +app.get('/api/blog/posts', (_req, res) => { + res.json(readDb().blogPosts.filter((p) => p.published)) +}) + +app.get('/api/blog/posts/all', authMiddleware, (_req, res) => { + res.json(readDb().blogPosts) +}) + +app.get('/api/blog/posts/slug/:slug', (req, res) => { + const post = readDb().blogPosts.find( + (p) => p.slug === req.params.slug && p.published + ) + if (!post) return res.status(404).json({ error: 'مطلب یافت نشد' }) + res.json(post) +}) + +app.get('/api/blog/posts/:id', (req, res) => { + const post = readDb().blogPosts.find((p) => p.id === req.params.id) + if (!post) return res.status(404).json({ error: 'مطلب یافت نشد' }) + res.json(post) +}) + +app.post('/api/blog/posts', authMiddleware, (req, res) => { + const post = { + id: randomUUID(), + published: false, + publishedAt: new Date().toISOString(), + ...req.body, + } + updateDb((db) => { + db.blogPosts.push(post) + }) + res.status(201).json(post) +}) + +app.put('/api/blog/posts/:id', authMiddleware, (req, res) => { + let updated = null + updateDb((db) => { + const i = db.blogPosts.findIndex((p) => p.id === req.params.id) + if (i === -1) return + db.blogPosts[i] = { ...db.blogPosts[i], ...req.body, id: req.params.id } + updated = db.blogPosts[i] + }) + if (!updated) return res.status(404).json({ error: 'مطلب یافت نشد' }) + res.json(updated) +}) + +app.delete('/api/blog/posts/:id', authMiddleware, (req, res) => { + let found = false + updateDb((db) => { + const i = db.blogPosts.findIndex((p) => p.id === req.params.id) + if (i !== -1) { + db.blogPosts.splice(i, 1) + found = true + } + }) + if (!found) return res.status(404).json({ error: 'مطلب یافت نشد' }) + res.status(204).end() +}) + +app.get('/api/media', authMiddleware, (_req, res) => { + res.json(readDb().media) +}) + +app.post('/api/media/upload', authMiddleware, upload.single('file'), (req, res) => { + if (!req.file) return res.status(400).json({ error: 'فایلی ارسال نشده' }) + const item = { + id: randomUUID(), + filename: req.file.originalname, + url: `/uploads/${req.file.filename}`, + mimeType: req.file.mimetype, + size: req.file.size, + createdAt: new Date().toISOString(), + } + updateDb((db) => { + db.media.unshift(item) + }) + res.status(201).json(item) +}) + +app.listen(PORT, '127.0.0.1', () => { + console.log(`Studio Noro API running at http://127.0.0.1:${PORT}`) +}) diff --git a/api/src/seed.js b/api/src/seed.js new file mode 100644 index 0000000..6d63f5e --- /dev/null +++ b/api/src/seed.js @@ -0,0 +1,123 @@ +import { existsSync, mkdirSync, copyFileSync, readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { writeDb } from './db.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const UPLOADS = join(__dirname, '..', 'uploads') +const NUXT = join(__dirname, '..', '..', 'nuxt-tailwind-app') + +function asset(rel, name) { + const src = join(NUXT, 'src', 'assets', 'images', rel) + if (!existsSync(UPLOADS)) mkdirSync(UPLOADS, { recursive: true }) + if (existsSync(src)) { + copyFileSync(src, join(UPLOADS, name)) + return `/uploads/${name}` + } + return '' +} + +export function runSeed() { + const cover = asset('portfolio/madar-biscuit-poster.png', 'portfolio-cover.png') + + const services = JSON.parse( + readFileSync(join(__dirname, 'services.seed.json'), 'utf-8') + ).map((s) => ({ + ...s, + iconUrl: asset(`services/${s.id === 'ui' ? 'ui' : s.id === 'graphic-design' ? 'graphic-design' : s.id === 'brand-identity' ? 'brand-identity' : s.id === 'social' ? 'social' : s.id === 'photography' ? 'photography' : 'video'}.png`, `icon-${s.id}.png`), + backgroundUrl: asset(`services/backgrounds/${s.id === 'ui' ? 'ui' : s.id === 'graphic-design' ? 'graphic-design' : s.id === 'brand-identity' ? 'brand-identity' : s.id === 'social' ? 'social' : s.id === 'photography' ? 'photography' : 'video'}.png`, `bg-${s.id}.png`), + portfolioCategorySlugs: + { + ui: ['uiux'], + 'graphic-design': ['graphic'], + 'brand-identity': ['logo', 'identity'], + social: ['social'], + photography: [], + video: ['motion'] + }[s.id] || [] + })) + + writeDb({ + site: { + name: 'استودیو نورو', + nameEn: 'Noro Studio', + tagline: 'استودیوی طراحی دیجیتال', + description: + 'استودیو نورو با تمرکز بر هویت بصری، UI/UX، طراحی سوشال مدیا و محتوای دیجیتال، برند شما را به تجربه‌ای مدرن و حرفه‌ای تبدیل می‌کند.', + email: 'hello@studionoro.ir', + website: 'studionoro.ir', + phone: '۰۹۳۳ ۲۰۶ ۱۶۶۷', + phoneTel: '+989332061667', + location: 'اصفهان، ایران', + messengerHandle: '@noro_vision', + messengersLabel: 'پیام‌رسان‌ها و شبکه‌های اجتماعی', + messengers: [ + { id: 'telegram', name: 'تلگرام', url: 'https://t.me/noro_vision', iconUrl: asset('messengers/telegram.png', 'tg.png') }, + { id: 'bale', name: 'بله', url: 'https://ble.ir/noro_vision', iconUrl: asset('messengers/bale.png', 'bale.png') }, + { id: 'rubika', name: 'روبیکا', url: 'https://rubika.ir/noro_vision', iconUrl: asset('messengers/rubika.png', 'rubika.png') }, + { id: 'instagram', name: 'اینستاگرام', url: 'https://www.instagram.com/noro.vision?igsh=MXF1NGE2NGMxOGxncA==', iconUrl: asset('messengers/instagram.svg', 'ig.svg') } + ], + seo: { + defaultTitle: 'استودیو نورو | Noro Studio', + defaultDescription: 'استودیو نورو — طراحی هویت بصری، UI/UX، سوشال مدیا و محتوای دیجیتال' + }, + hero: { + darkImageUrl: asset('hero/studio-background.png', 'hero-dark.png'), + lightImageUrl: asset('hero/studio-background-light.png', 'hero-light.png') + } + }, + navigation: [ + { label: 'خانه', to: '/' }, + { label: 'نمونه‌کارها', to: '/portfolio' }, + { label: 'خدمات', to: '/services' }, + { label: 'وبلاگ', to: '/blog' }, + { label: 'تماس', to: '/contact' } + ], + portfolioCategories: [ + { slug: 'graphic', label: 'طراحی گرافیک' }, + { slug: 'logo', label: 'طراحی لوگو' }, + { slug: 'identity', label: 'هویت بصری' }, + { slug: 'uiux', label: 'UI/UX' }, + { slug: 'social', label: 'طراحی سوشال مدیا' }, + { slug: 'motion', label: 'تدوین و موشن گرافیک' } + ], + projects: [ + { id: '1', title: 'طراحی پوستر تبلیغاتی بیسکویت مادر', category: 'graphic', description: 'این پوستر به سفارش شرکت بیسکویت مادر طراحی شده است.', longDescription: 'پروژه پوستر تبلیغاتی بیسکویت مادر با هدف معرفی خواص غذایی و ارزش‌های برند طراحی شد.', year: 'خرداد ۱۴۰۴', featured: true, imageUrl: cover, galleryUrls: [cover, cover], client: 'بیسکویت مادر', deliverables: ['پوستر تبلیغاتی'], seoTitle: '', seoDescription: '' }, + { id: '2', title: 'اپلیکیشن فین‌تک', category: 'uiux', description: 'طراحی رابط و تجربه کاربری اپ موبایل مالی', longDescription: 'طراحی کامل UI/UX اپلیکیشن موبایل فین‌تک.', year: '۱۴۰۴', featured: true, imageUrl: cover, galleryUrls: [cover], client: 'استارتاپ فین‌تک', deliverables: ['طراحی UI'], seoTitle: '', seoDescription: '' }, + { id: '3', title: 'لوگو استارتاپ نوا', category: 'logo', description: 'طراحی لوگوتایپ مینیمال', longDescription: 'لوگوتایپ استارتاپ نوا با رویکرد مینیمال طراحی شد.', year: '۱۴۰۳', featured: true, imageUrl: cover, galleryUrls: [cover], client: 'استارتاپ نوا', deliverables: ['لوگوتایپ'], seoTitle: '', seoDescription: '' }, + { id: '4', title: 'کمپین اینستاگرام', category: 'social', description: 'طراحی قالب و محتوای بصری', longDescription: 'کمپین بصری اینستاگرام برای فروش فصلی.', year: '۱۴۰۳', featured: false, imageUrl: cover, galleryUrls: [cover], client: 'برند خرده‌فروشی', deliverables: ['قالب پست'], seoTitle: '', seoDescription: '' }, + { id: '5', title: 'موشن معرفی محصول', category: 'motion', description: 'تدوین و موشن گرافیک', longDescription: 'ویدیوی موشن گرافیک ۶۰ ثانیه‌ای.', year: '۱۴۰۳', featured: false, imageUrl: cover, galleryUrls: [cover], client: 'برند لوازم خانگی', deliverables: ['انیمیشن'], seoTitle: '', seoDescription: '' }, + { id: '6', title: 'پنل مدیریت SaaS', category: 'uiux', description: 'طراحی داشبورد SaaS', longDescription: 'طراحی داشبورد مدیریتی SaaS.', year: '۱۴۰۲', featured: false, imageUrl: cover, galleryUrls: [cover], client: 'پلتفرم SaaS', deliverables: ['داشبورد'], seoTitle: '', seoDescription: '' } + ], + services, + serviceProjectCategories: { + ui: ['uiux'], + 'graphic-design': ['graphic'], + 'brand-identity': ['logo', 'identity'], + social: ['social'], + photography: [], + video: ['motion'] + }, + clients: [ + { id: 'chehelsotoun-souleh', name: 'چهلستون سوله اصفهان', logoUrl: asset('clients/chehelsotoun-souleh.png', 'c1.png') }, + { id: 'montzar', name: 'منتظر', logoUrl: asset('clients/montzar.png', 'c2.png') }, + { id: 'madar', name: 'مادر', logoUrl: asset('clients/madar.png', 'c3.png') }, + { id: 'azad-khorasgan', name: 'دانشگاه آزاد اسلامی — واحد خوراسگان', logoUrl: asset('clients/azad-khorasgan.png', 'c4.png') }, + { id: 'isfahan', name: 'اصفهان', logoUrl: asset('clients/isfahan.png', 'c5.png') }, + { id: 'montech', name: 'منتک', logoUrl: asset('clients/montech.png', 'c6.png') }, + { id: 'farshchin', name: 'فرشچین', logoUrl: asset('clients/farshchin.png', 'c7.png') }, + { id: 'rouyesh', name: 'رویش — گروه توسعه نرم‌افزاری', logoUrl: asset('clients/rouyesh.png', 'c8.png') } + ], + stats: [ + { value: '۵۰+', label: 'پروژه انجام‌شده' }, + { value: '۳۰+', label: 'مشتری راضی' }, + { value: '۶', label: 'حوزه تخصصی' } + ], + blogPosts: [], + media: [] + }) + + console.log('Seed completed.') +} + +runSeed() diff --git a/api/src/services.seed.json b/api/src/services.seed.json new file mode 100644 index 0000000..40e0b0e --- /dev/null +++ b/api/src/services.seed.json @@ -0,0 +1,260 @@ +[ + { + "id": "ui", + "title": "طراحی رابط کاربری (UI/UX)", + "subtitle": "طراحی رابط کاربری حرفه‌ای برای وب‌سایت و اپلیکیشن", + "description": "طراحی رابط کاربری برای وب، موبایل و محصولات دیجیتال با تمرکز بر تجربه کاربری.", + "intro": "ما رابط‌های کاربری زیبا، کاربردی و کاربرمحور طراحی می‌کنیم تا کاربران تجربه‌ای روان و لذت‌بخش از تعامل با محصول شما داشته باشند. هدف ما تنها زیبایی نیست؛ بلکه ایجاد تجربه‌ای است که به افزایش تعامل کاربران و رشد کسب‌وکار شما کمک کند.", + "includes": [ + "تحقیق و تحلیل کاربران", + "طراحی User Flow", + "طراحی Wireframe", + "طراحی رابط کاربری صفحات", + "طراحی نسخه موبایل و دسکتاپ", + "طراحی سیستم طراحی (Design System)", + "پروتوتایپ تعاملی" + ], + "benefits": [ + "افزایش نرخ تبدیل کاربران", + "بهبود تجربه کاربری", + "کاهش سردرگمی کاربران", + "افزایش اعتماد به برند", + "آماده‌سازی محصول برای توسعه حرفه‌ای" + ], + "process": [ + { "title": "شناخت پروژه", "description": "بررسی اهداف، مخاطبان و نیازهای محصول." }, + { "title": "طراحی ساختار", "description": "طراحی مسیرهای کاربری و وایرفریم صفحات." }, + { "title": "طراحی بصری", "description": "طراحی رابط کاربری بر اساس هویت برند." }, + { "title": "بازبینی و اصلاح", "description": "دریافت بازخورد و اعمال تغییرات." }, + { "title": "تحویل نهایی", "description": "ارائه فایل‌های طراحی و مستندات لازم." } + ], + "deliverables": [ + "طراحی صفحات وب‌سایت", + "داشبوردهای مدیریتی", + "اپلیکیشن‌های موبایل", + "لندینگ پیج‌ها", + "سیستم‌های نرم‌افزاری" + ], + "faq": [ + { + "question": "آیا طراحی برای موبایل هم انجام می‌شود؟", + "answer": "بله، تمامی پروژه‌ها به صورت Responsive طراحی می‌شوند." + }, + { + "question": "فایل نهایی در چه فرمتی تحویل داده می‌شود؟", + "answer": "فایل‌های Figma و تمامی Assetهای مورد نیاز تحویل داده می‌شوند." + } + ] + }, + { + "id": "graphic-design", + "title": "طراحی گرافیک", + "subtitle": "خلق هویت بصری جذاب برای برندها", + "description": "خلق اثرهای بصری حرفه‌ای برای چاپ، تبلیغات، بسته‌بندی و کمپین‌های برند.", + "intro": "طراحی گرافیک تنها ساخت تصاویر زیبا نیست؛ بلکه ابزاری برای انتقال پیام برند و ایجاد ارتباط مؤثر با مخاطبان است.", + "includes": [ + "طراحی پوستر", + "طراحی بنر تبلیغاتی", + "طراحی بسته‌بندی", + "طراحی کاتالوگ و بروشور", + "طراحی استوری و پست شبکه‌های اجتماعی", + "طراحی اقلام چاپی" + ], + "benefits": [ + "افزایش جذابیت برند", + "انتقال بهتر پیام", + "ایجاد تمایز در بازار", + "افزایش اثربخشی کمپین‌های تبلیغاتی" + ], + "process": [ + { "title": "دریافت نیازمندی‌ها", "description": "جمع‌آوری اطلاعات و اهداف پروژه." }, + { "title": "تحقیق و ایده‌پردازی", "description": "بررسی روندها و تولید مفاهیم اولیه." }, + { "title": "طراحی اولیه", "description": "ارائه طرح‌های اولیه برای بازبینی." }, + { "title": "اصلاح و بهینه‌سازی", "description": "اعمال بازخورد و بهبود طرح." }, + { "title": "تحویل نهایی", "description": "تحویل فایل‌های نهایی آماده استفاده." } + ], + "deliverables": ["پوستر", "بنر", "کاتالوگ", "بسته‌بندی", "طراحی کمپین تبلیغاتی"], + "faq": [ + { + "question": "آیا فایل قابل چاپ تحویل داده می‌شود؟", + "answer": "بله، تمامی فایل‌ها با استاندارد چاپ آماده می‌شوند." + }, + { + "question": "چند مرحله اصلاح وجود دارد؟", + "answer": "بسته به پروژه، چند مرحله بازبینی در نظر گرفته می‌شود." + } + ] + }, + { + "id": "brand-identity", + "title": "هویت برند", + "subtitle": "ساخت تصویری ماندگار برای برند شما", + "description": "طراحی سیستم برند یکپارچه شامل لوگو، رنگ، تایپوگرافی و راهنمای هویت بصری.", + "intro": "هویت برند مجموعه‌ای از عناصر بصری و مفهومی است که شخصیت برند شما را شکل می‌دهد و باعث می‌شود در ذهن مخاطب ماندگار شوید.", + "includes": [ + "طراحی لوگو", + "طراحی پالت رنگی", + "انتخاب تایپوگرافی", + "طراحی آیکون‌ها", + "طراحی المان‌های گرافیکی", + "تدوین Brand Guideline" + ], + "benefits": [ + "افزایش اعتبار برند", + "ایجاد یکپارچگی بصری", + "متمایز شدن از رقبا", + "افزایش اعتماد مخاطبان" + ], + "process": [ + { "title": "شناخت برند", "description": "درک ارزش‌ها، مخاطب و اهداف برند." }, + { "title": "تحلیل بازار و رقبا", "description": "بررسی فضای رقابتی و فرصت‌ها." }, + { "title": "طراحی مفاهیم اولیه", "description": "ارائه چند مفهوم بصری برای انتخاب." }, + { "title": "توسعه هویت بصری", "description": "تکمیل لوگو، رنگ، تایپوگرافی و المان‌ها." }, + { "title": "تدوین راهنمای برند", "description": "مستندسازی قوانین استفاده از هویت." } + ], + "deliverables": ["لوگو", "برندبوک", "کارت ویزیت", "ست اداری", "دارایی‌های دیجیتال برند"], + "faq": [ + { + "question": "آیا برندبوک هم ارائه می‌شود؟", + "answer": "بله، راهنمای کامل استفاده از هویت بصری ارائه خواهد شد." + }, + { + "question": "اگر لوگوی قبلی داشته باشیم چه؟", + "answer": "امکان بازطراحی یا توسعه هویت موجود وجود دارد." + } + ] + }, + { + "id": "social", + "title": "سوشال مدیا", + "subtitle": "مدیریت و تولید محتوای حرفه‌ای برای شبکه‌های اجتماعی", + "description": "طراحی و تولید محتوای بصری برای اینستاگرام، لینکدین و پلتفرم‌های اجتماعی.", + "intro": "حضور مؤثر در شبکه‌های اجتماعی نیازمند برنامه‌ریزی، طراحی و تولید محتوای هدفمند است. ما به برند شما کمک می‌کنیم تا ارتباطی مستمر و مؤثر با مخاطبان خود داشته باشد.", + "includes": [ + "طراحی پست و استوری", + "تدوین تقویم محتوایی", + "طراحی قالب اختصاصی", + "تولید محتوای بصری", + "مشاوره استراتژی محتوا", + "مدیریت کمپین‌های شبکه اجتماعی" + ], + "benefits": [ + "افزایش آگاهی از برند", + "رشد تعامل کاربران", + "جذب مخاطب هدف", + "ایجاد هویت بصری منسجم" + ], + "process": [ + { "title": "تحلیل وضعیت فعلی", "description": "بررسی حضور فعلی برند در شبکه‌ها." }, + { "title": "تدوین استراتژی محتوا", "description": "تعیین اهداف، لحن و برنامه محتوایی." }, + { "title": "طراحی قالب‌ها", "description": "ساخت قالب‌های بصری یکپارچه." }, + { "title": "تولید محتوا", "description": "طراحی و تولید پست، استوری و محتوای تعاملی." }, + { "title": "انتشار و بهینه‌سازی", "description": "انتشار محتوا و تحلیل عملکرد." } + ], + "deliverables": [ + "پست اینستاگرام", + "استوری", + "کاور لینکدین", + "بنر کمپین", + "ریلز و محتوای تعاملی" + ], + "faq": [ + { + "question": "برای چه پلتفرم‌هایی تولید محتوا انجام می‌شود؟", + "answer": "اینستاگرام، لینکدین و سایر شبکه‌های اجتماعی." + }, + { + "question": "آیا استراتژی محتوا هم ارائه می‌شود؟", + "answer": "بله، پیش از تولید محتوا برنامه محتوایی تدوین می‌شود." + } + ] + }, + { + "id": "photography", + "title": "عکاسی", + "subtitle": "ثبت حرفه‌ای محصولات و برندها", + "description": "عکاسی حرفه‌ای محصول، پرتره و محتوای بصری برای استفاده در برند و تبلیغات.", + "intro": "تصاویر حرفه‌ای یکی از مهم‌ترین عوامل تأثیرگذاری بر تصمیم مخاطبان هستند. ما با عکاسی حرفه‌ای به نمایش بهتر محصولات و خدمات شما کمک می‌کنیم.", + "includes": [ + "عکاسی محصول", + "عکاسی تبلیغاتی", + "عکاسی صنعتی", + "عکاسی برندینگ", + "ویرایش و روتوش تصاویر" + ], + "benefits": [ + "افزایش کیفیت نمایش محصولات", + "جلب اعتماد مشتریان", + "تولید محتوای حرفه‌ای", + "ارتقای تصویر برند" + ], + "process": [ + { "title": "برنامه‌ریزی پروژه", "description": "هماهنگی اهداف، سبک و زمان‌بندی." }, + { "title": "آماده‌سازی صحنه", "description": "چیدمان نور، پس‌زمینه و تجهیزات." }, + { "title": "عکاسی", "description": "اجرای جلسه عکاسی حرفه‌ای." }, + { "title": "ویرایش تصاویر", "description": "رنگ‌بندی، روتوش و بهینه‌سازی." }, + { "title": "تحویل نهایی", "description": "تحویل تصاویر با کیفیت مناسب هر رسانه." } + ], + "deliverables": [ + "تصاویر فروشگاهی", + "عکس‌های تبلیغاتی", + "تصاویر وب‌سایت", + "تصاویر شبکه‌های اجتماعی" + ], + "faq": [ + { + "question": "آیا ادیت تصاویر هم انجام می‌شود؟", + "answer": "بله، تمامی تصاویر به صورت حرفه‌ای ویرایش می‌شوند." + }, + { + "question": "امکان عکاسی در محل مشتری وجود دارد؟", + "answer": "بله، در صورت نیاز پروژه در محل اجرا می‌شود." + } + ] + }, + { + "id": "video", + "title": "تدوین و محتوای ویدیویی", + "subtitle": "روایت حرفه‌ای داستان برند شما با ویدئو", + "description": "تدوین، موشن گرافیک و تولید محتوای ویدیویی برای معرفی برند و کمپین‌ها.", + "intro": "ویدئو یکی از قدرتمندترین ابزارهای ارتباطی است. ما با تولید و تدوین محتوای ویدیویی حرفه‌ای به برند شما کمک می‌کنیم تا پیام خود را تأثیرگذارتر منتقل کند.", + "includes": [ + "تدوین ویدئو", + "ساخت تیزر تبلیغاتی", + "موشن گرافیک", + "ویدئوهای شبکه اجتماعی", + "اصلاح رنگ و صدا", + "ساخت ویدئوهای معرفی برند" + ], + "benefits": [ + "افزایش تعامل مخاطبان", + "انتقال مؤثر پیام برند", + "افزایش نرخ تبدیل", + "ارتقای کیفیت محتوای دیجیتال" + ], + "process": [ + { "title": "دریافت نیاز پروژه", "description": "شناخت اهداف و مخاطب ویدئو." }, + { "title": "سناریو و برنامه‌ریزی", "description": "تدوین سناریو و استوری‌بورد." }, + { "title": "تولید یا دریافت محتوا", "description": "فیلم‌برداری یا دریافت فوتیج." }, + { "title": "تدوین و اصلاح", "description": "تدوین، موشن و اصلاح رنگ و صدا." }, + { "title": "خروجی نهایی", "description": "تحویل ویدئو در فرمت‌های مورد نیاز." } + ], + "deliverables": [ + "تیزر تبلیغاتی", + "ویدئوی معرفی برند", + "موشن گرافیک", + "ریلز اینستاگرام", + "ویدئوهای آموزشی" + ], + "faq": [ + { + "question": "خروجی برای شبکه‌های اجتماعی هم آماده می‌شود؟", + "answer": "بله، ویدئوها متناسب با هر پلتفرم خروجی گرفته می‌شوند." + }, + { + "question": "آیا موشن گرافیک هم ارائه می‌شود؟", + "answer": "بله، طراحی و اجرای موشن گرافیک بخشی از این خدمت است." + } + ] + } +] diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..43c9934 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_ROOT="${APP_ROOT:-/var/www/studionoro}" + +echo "==> Pull latest code (if using git)..." +cd "$APP_ROOT" +git pull || true + +echo "==> Rebuild API..." +cd "$APP_ROOT/api" +npm ci --omit=dev || npm install --omit=dev + +echo "==> Rebuild Nuxt..." +cd "$APP_ROOT/nuxt-tailwind-app" +npm ci || npm install +npm run build + +echo "==> Rebuild admin..." +cd "$APP_ROOT/admin" +npm ci || npm install +npm run build + +echo "==> Restart services..." +cd "$APP_ROOT" +pm2 restart ecosystem.config.cjs +systemctl reload nginx + +echo "Deploy complete." diff --git a/deploy/env/nuxt.env.example b/deploy/env/nuxt.env.example new file mode 100644 index 0000000..2268b8d --- /dev/null +++ b/deploy/env/nuxt.env.example @@ -0,0 +1,2 @@ +NUXT_PUBLIC_CMS_API_URL=https://api.studionoro.ir +NODE_ENV=production diff --git a/deploy/nginx/studionoro.conf b/deploy/nginx/studionoro.conf new file mode 100644 index 0000000..e6481d6 --- /dev/null +++ b/deploy/nginx/studionoro.conf @@ -0,0 +1,84 @@ +# Nginx site config for studionoro.ir +# Copy to: /etc/nginx/sites-available/studionoro +# Enable: sudo ln -sf /etc/nginx/sites-available/studionoro /etc/nginx/sites-enabled/ + +upstream studionoro_web { + server 127.0.0.1:3000; + keepalive 32; +} + +upstream studionoro_api { + server 127.0.0.1:4000; + keepalive 32; +} + +# --- Main website (Nuxt) --- +server { + listen 80; + listen [::]:80; + server_name studionoro.ir www.studionoro.ir; + + client_max_body_size 20M; + + location / { + proxy_pass http://studionoro_web; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + } +} + +# --- Admin panel (Vue static build) --- +server { + listen 80; + listen [::]:80; + server_name admin.studionoro.ir; + + root /var/www/studionoro/admin/dist; + index index.html; + client_max_body_size 20M; + + location /api/ { + proxy_pass http://studionoro_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /uploads/ { + proxy_pass http://studionoro_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + location / { + try_files $uri $uri/ /index.html; + } +} + +# --- API backend --- +server { + listen 80; + listen [::]:80; + server_name api.studionoro.ir; + + client_max_body_size 20M; + + location / { + proxy_pass http://studionoro_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/deploy/pm2/ecosystem.config.cjs b/deploy/pm2/ecosystem.config.cjs new file mode 100644 index 0000000..60d0d49 --- /dev/null +++ b/deploy/pm2/ecosystem.config.cjs @@ -0,0 +1,31 @@ +module.exports = { + apps: [ + { + name: 'studionoro-api', + cwd: '/var/www/studionoro/api', + script: 'src/index.js', + instances: 1, + autorestart: true, + max_memory_restart: '300M', + env: { + NODE_ENV: 'production', + PORT: 4000, + }, + }, + { + name: 'studionoro-web', + cwd: '/var/www/studionoro/nuxt-tailwind-app', + script: '.output/server/index.mjs', + instances: 1, + autorestart: true, + max_memory_restart: '512M', + env: { + NODE_ENV: 'production', + PORT: 3000, + HOST: '127.0.0.1', + NITRO_HOST: '127.0.0.1', + NITRO_PORT: '3000', + }, + }, + ], +} diff --git a/deploy/setup-vps.sh b/deploy/setup-vps.sh new file mode 100644 index 0000000..936c8df --- /dev/null +++ b/deploy/setup-vps.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Studio Noro — VPS bootstrap (Ubuntu 22.04/24.04) +# Run as root or with sudo on a fresh VPS after uploading the project. + +APP_ROOT="${APP_ROOT:-/var/www/studionoro}" +DOMAIN="${DOMAIN:-studionoro.ir}" +ADMIN_DOMAIN="${ADMIN_DOMAIN:-admin.studionoro.ir}" +API_DOMAIN="${API_DOMAIN:-api.studionoro.ir}" + +echo "==> Installing system packages..." +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y curl git nginx ufw certbot python3-certbot-nginx + +echo "==> Installing Node.js 20 LTS..." +if ! command -v node >/dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs +fi + +echo "==> Installing PM2..." +npm install -g pm2 + +echo "==> Configuring firewall..." +ufw allow OpenSSH +ufw allow 'Nginx Full' +ufw --force enable + +echo "==> Creating app directory..." +mkdir -p "$APP_ROOT" + +if [ ! -d "$APP_ROOT/api" ]; then + echo "ERROR: Project files not found in $APP_ROOT" + echo "Upload or clone your project to $APP_ROOT first." + exit 1 +fi + +echo "==> Installing API dependencies..." +cd "$APP_ROOT/api" +if [ ! -f .env ]; then + cp .env.example .env + echo "WARNING: Edit $APP_ROOT/api/.env before going live (JWT_SECRET, ADMIN_PASSWORD)." +fi +npm ci --omit=dev || npm install --omit=dev + +echo "==> Seeding API data (first run)..." +npm run seed || true + +echo "==> Building Nuxt frontend..." +cd "$APP_ROOT/nuxt-tailwind-app" +if [ ! -f .env ]; then + cp "$APP_ROOT/deploy/env/nuxt.env.example" .env +fi +npm ci || npm install +npm run build + +echo "==> Building admin panel..." +cd "$APP_ROOT/admin" +npm ci || npm install +npm run build + +echo "==> Installing Nginx config..." +cp "$APP_ROOT/deploy/nginx/studionoro.conf" /etc/nginx/sites-available/studionoro +ln -sf /etc/nginx/sites-available/studionoro /etc/nginx/sites-enabled/studionoro +rm -f /etc/nginx/sites-enabled/default +nginx -t +systemctl reload nginx + +echo "==> Starting PM2 apps..." +cp "$APP_ROOT/deploy/pm2/ecosystem.config.cjs" "$APP_ROOT/ecosystem.config.cjs" +cd "$APP_ROOT" +pm2 start ecosystem.config.cjs +pm2 save +pm2 startup systemd -u root --hp /root || pm2 startup + +echo "==> Requesting SSL certificates..." +certbot --nginx \ + -d "$DOMAIN" -d "www.$DOMAIN" \ + -d "$ADMIN_DOMAIN" \ + -d "$API_DOMAIN" \ + --non-interactive --agree-tos -m "admin@$DOMAIN" || \ + echo "Certbot failed — set DNS first, then run certbot manually." + +echo "" +echo "Done. Verify:" +echo " https://$DOMAIN" +echo " https://$ADMIN_DOMAIN" +echo " https://$API_DOMAIN/health" diff --git a/deploy/upload-notes.txt b/deploy/upload-notes.txt new file mode 100644 index 0000000..f06c469 --- /dev/null +++ b/deploy/upload-notes.txt @@ -0,0 +1,14 @@ +# Copy project to VPS (run from Windows PowerShell in C:\Users\SMB\Projects) + +# Option A — using SCP (replace VPS_IP and user): +# scp -r api admin nuxt-tailwind-app deploy user@VPS_IP:/var/www/studionoro/ + +# Option B — using Git on VPS (recommended): +# 1. Push this repo to GitHub/GitLab +# 2. On VPS: git clone YOUR_REPO_URL /var/www/studionoro + +# After files are on VPS: +# ssh user@VPS_IP +# cd /var/www/studionoro +# chmod +x deploy/setup-vps.sh deploy/deploy.sh +# sudo bash deploy/setup-vps.sh