Initial commit: API and deploy configs

This commit is contained in:
Noro Studio
2026-06-30 00:51:32 +03:30
commit b0361594b1
15 changed files with 985 additions and 0 deletions
+7
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
node_modules/
.env
data/
uploads/
+20
View File
@@ -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"
}
}
+35
View File
@@ -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 })
}
+46
View File
@@ -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
}
+239
View File
@@ -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}`)
})
+123
View File
@@ -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()
+260
View File
@@ -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": "بله، طراحی و اجرای موشن گرافیک بخشی از این خدمت است."
}
]
}
]
+1
View File
@@ -0,0 +1 @@
*.log
+29
View File
@@ -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."
+2
View File
@@ -0,0 +1,2 @@
NUXT_PUBLIC_CMS_API_URL=https://api.studionoro.ir
NODE_ENV=production
+84
View File
@@ -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;
}
}
+31
View File
@@ -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',
},
},
],
}
+90
View File
@@ -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"
+14
View File
@@ -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