Compare commits
No commits in common. "8d6d7d6a53d3661eaff4d4d1843f76f951398551" and "75f35b81833829224d529933288d8fbd5023bb52" have entirely different histories.
8d6d7d6a53
...
75f35b8183
@ -1,5 +1,7 @@
|
||||
node_modules
|
||||
# dist
|
||||
.env
|
||||
.git
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
backend
|
||||
|
||||
@ -5,10 +5,18 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: canina
|
||||
steps:
|
||||
- name: Setup SSH key
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: sh
|
||||
steps:
|
||||
- name: Install SSH client
|
||||
run: |
|
||||
V=$(cut -d'.' -f1-2 /etc/alpine-release)
|
||||
echo "https://dl-cdn.alpinelinux.org/alpine/v$V/main" > /etc/apk/repositories
|
||||
echo "https://dl-cdn.alpinelinux.org/alpine/v$V/community" >> /etc/apk/repositories
|
||||
apk add --no-cache openssh-client
|
||||
- name: Setup SSH key
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||
run: |
|
||||
@ -17,5 +25,4 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -p 2234 -H 172.17.0.1 >> ~/.ssh/known_hosts
|
||||
- name: Deploy
|
||||
shell: sh
|
||||
run: ssh -p 2234 devops@172.17.0.1 'bash /opt/services/scripts/depcanina.sh main'
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@ -2,7 +2,7 @@ FROM node:22-alpine AS app-builder
|
||||
RUN apk add --no-cache openssl
|
||||
WORKDIR /app
|
||||
COPY frontend/application/package*.json ./
|
||||
RUN npm config set registry http://registry.parsaaghayi.ir/repository/npm-proxy/ && npm ci
|
||||
RUN npm ci
|
||||
COPY frontend/application ./
|
||||
RUN npm run build
|
||||
|
||||
@ -10,16 +10,15 @@ FROM node:22-alpine AS admin-builder
|
||||
RUN apk add --no-cache openssl
|
||||
WORKDIR /app
|
||||
COPY frontend/admin-panel/package*.json ./
|
||||
RUN npm config set registry http://registry.parsaaghayi.ir/repository/npm-proxy/ && npm ci
|
||||
RUN npm ci
|
||||
COPY frontend/admin-panel ./
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
RUN apk add --no-cache nginx
|
||||
WORKDIR /app
|
||||
COPY --from=app-builder /app .
|
||||
COPY --from=admin-builder /app/dist ./admin_static
|
||||
COPY nginx.conf /etc/nginx/http.d/default.conf
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=app-builder /app/.next ./.next
|
||||
COPY --from=app-builder /app/public ./public
|
||||
COPY --from=admin-builder /app/dist ./admin
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 8080
|
||||
CMD ["sh", "-c", "nginx && npm start"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.git
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm config set registry http://registry.parsaaghayi.ir/repository/npm-proxy/ && npm ci
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npx prisma generate && npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
RUN mkdir -p /app/uploads && chown node:node /app/uploads
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package*.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/prisma/scientificTerms.ts ./prisma/scientificTerms.ts
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER node
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/main"]
|
||||
|
||||
CMD ["node", "dist/main"]
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
export interface ScientificTerm {
|
||||
term: string;
|
||||
definition: string;
|
||||
wikiId?: string;
|
||||
}
|
||||
|
||||
export const SCIENTIFIC_TERMS: Record<string, ScientificTerm> = {
|
||||
"گلیکوزآمینوگلیکان": {
|
||||
term: "گلیکوزآمینوگلیکان (GAG)",
|
||||
definition: "ترکیبات ساختاری ضروری برای بازسازی غضروفها و مایع مفصلی که در Canhydrox GAG به وفور یافت میشود.",
|
||||
wikiId: "gag"
|
||||
},
|
||||
"اسیدهای هومیک": {
|
||||
term: "اسیدهای هومیک",
|
||||
definition: "مواد آلی طبیعی موجود در عصاره پیت (Moor) که از مخاط روده محافظت کرده و سموم را جذب میکنند.",
|
||||
wikiId: "humic-acids"
|
||||
},
|
||||
"کلاژن زیستفعال": {
|
||||
term: "کلاژن زیستفعال",
|
||||
definition: "پروتئینهای اختصاصی هیدرولیز شده که به سرعت جذب استخوان و تاندون شده و استحکام ساختاری ایجاد میکنند.",
|
||||
wikiId: "collagen"
|
||||
},
|
||||
"تیموکینون": {
|
||||
term: "تیموکینون",
|
||||
definition: "ماده موثره اصلی در روغن سیاه دانه (Nigella sativa) با خواص ضدالتهابی، ضدآلرژی و آنتیاکسیدان. با مهار مسیر NF-κB تولید سایتوکاینهای پیشالتهابی را کاهش میدهد.",
|
||||
wikiId: "thymoquinone"
|
||||
},
|
||||
"امگا ۳": {
|
||||
term: "امگا ۳",
|
||||
definition: "اسیدهای چرب ضروری شامل EPA و DHA که برای سلامت پوست، مو، قلب و عملکرد شناختی حیاتی هستند. EPA پیشماده رزولوینهای ضدالتهابی و DHA ۳۰٪ وزن خشک مغز را تشکیل میدهد.",
|
||||
wikiId: "omega3"
|
||||
},
|
||||
"میکروسیلور": {
|
||||
term: "نقره میکروسیلور (Microsilver BG)",
|
||||
definition: "ذرات نقره خالص با ساختار اسفنجی که بدون جذب در خون، اثر آنتیباکتریال پایداری بر سطح پوست و لثه دارند.",
|
||||
wikiId: "silver"
|
||||
},
|
||||
"صدف لبسبز": {
|
||||
term: "صدف لبسبز (Green-lipped Mussel)",
|
||||
definition: "صدف بومی نیوزلند حاوی گلوکوزامینگلیکانهای طبیعی که نقش حیاتی در بازسازی غضروف و مفاصل دارند.",
|
||||
wikiId: "joints"
|
||||
},
|
||||
"نسبت Ca:P": {
|
||||
term: "نسبت کلسیم به فسفر (Ca:P Ratio)",
|
||||
definition: "نسبت ایدهآل کلسیم به فسفر در رژیم حیوانات ۱.۲:۱ تا ۱.۵:۱ است. عدم تعادل این نسبت منجر به مشکلات کلیوی و استخوانی میشود. پودر پوسته تخم مرغ کانینا با ۳۶٪ کلسیم و ۰.۱٪ فسفر بهترین تنظیمکننده این نسبت است.",
|
||||
wikiId: "nutrition"
|
||||
},
|
||||
"ویتامین D3": {
|
||||
term: "ویتامین D3 (کولهکلسیفرول)",
|
||||
definition: "فرم فعال ویتامین D که جذب کلسیم از روده را تا ۳۰٪ افزایش میدهد. هشدار: مصرف همزمان با ویتامین D2 (ارگوکلسیفرول) مجاز نیست زیرا رقابت این دو جذب مؤثر را کاهش میدهد.",
|
||||
wikiId: "vitamins"
|
||||
},
|
||||
"BARF": {
|
||||
term: "رژیم BARF (Biologically Appropriate Raw Food)",
|
||||
definition: "رژیم غذایی خام متناسب با زیستشناسی حیوان که شامل گوشت خام، استخوان، اندامها و سبزیجات است. مکملهای کانینا مانند Eierschalenpulver، Rinderblut Pulver و Rinderfett Pulver برای تکمیل این رژیم طراحی شدهاند.",
|
||||
wikiId: "nutrition"
|
||||
},
|
||||
"آغوز": {
|
||||
term: "آغوز (Colostrum)",
|
||||
definition: "اولین شیر پستانداران حاوی IgG, IgA, IgM و لاکتوفرین که سد دفاعی روده را تقویت کرده و ایمنی غیرفعال منتقل میکند. محصول Immun-Booster کانینا حاوی ۲۰٪ پودر آغوز است.",
|
||||
wikiId: "immune"
|
||||
},
|
||||
"ال-کارنیتین": {
|
||||
term: "ال-کارنیتین",
|
||||
definition: "مادهای شبهآمینواسید که اسیدهای چرب زنجیره بلند را به داخل میتوکندری کاردیومیوسیتها منتقل کرده و ATP تولید میکند. محصول Herz Vital کانینا حاوی ۳۰,۰۰۰mg/kg ال-کارنیتین است.",
|
||||
wikiId: "heart"
|
||||
},
|
||||
"زالزالک": {
|
||||
term: "زالزالک (Crataegus oxyacantha)",
|
||||
definition: "گیاه دارویی با اثر مهارکننده ACE طبیعی که جریان خون کرونر را بهبود بخشیده و فشار بر قلب را کاهش میدهد.",
|
||||
wikiId: "heart"
|
||||
},
|
||||
"بیوتین": {
|
||||
term: "بیوتین (ویتامین H / B7)",
|
||||
definition: "کوآنزیم کربوکسیلازهای مسئول سنتز اسیدهای چرب و گلوکونئوژنز. کمبود آن منجر به ریزش مو و شکنندگی پنجه میشود. قرص Biotin-Tabs کانینا با ۱,۰۰۰,۰۰۰ میکروگرم در کیلوگرم قویترین دوز موجود است.",
|
||||
wikiId: "skin"
|
||||
},
|
||||
"هیدروکسی آپاتیت": {
|
||||
term: "هیدروکسی آپاتیت",
|
||||
definition: "کلسیم فسفات بلوری که ۹۰٪ بافت دندان و بخش عمده استخوان را تشکیل میدهد. با جذب مستقیم در توده استخوانی، استحکام اسکلت را تضمین میکند.",
|
||||
wikiId: "joints"
|
||||
},
|
||||
"EPA/DHA": {
|
||||
term: "EPA و DHA",
|
||||
definition: "ایکوزاپنتائنوئیک اسید (EPA) پیشماده رزولوینهای ضدالتهابی و دوکوزاهگزانوئیک اسید (DHA) ۳۰٪ وزن خشک مغز را تشکیل میدهد. نسبت ۳:۲ EPA:DHA در روغن سالمون کانینا بهینهسازی شده است.",
|
||||
wikiId: "omega3"
|
||||
}
|
||||
};
|
||||
@ -55,7 +55,7 @@ async function main() {
|
||||
console.log('Seeding products from seed-products-data.json...');
|
||||
|
||||
// 2. Read JSON data
|
||||
const dataPath = path.join(process.cwd() + "/prisma", 'seed-products-data.json');
|
||||
const dataPath = path.join(__dirname, 'seed-products-data.json');
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
console.error('seed-products-data.json not found at', dataPath);
|
||||
process.exit(1);
|
||||
|
||||
@ -165,7 +165,7 @@ async function main() {
|
||||
|
||||
// 2. Seed Scientific Terms
|
||||
console.log('Seeding Scientific Terms...');
|
||||
const termsFilePath = path.join(process.cwd() + "/prisma", '..', '..', 'frontend', 'application', 'lib', 'data', 'scientificTerms.ts');
|
||||
const termsFilePath = path.join(__dirname, '..', '..', 'frontend', 'application', 'lib', 'data', 'scientificTerms.ts');
|
||||
if (fs.existsSync(termsFilePath)) {
|
||||
const termsContent = fs.readFileSync(termsFilePath, 'utf-8');
|
||||
const termsJsCode = ts.transpile(termsContent, {
|
||||
|
||||
@ -46,4 +46,3 @@ async function bootstrap() {
|
||||
await app.listen(process.env.PORT ?? 4001);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
|
||||
@ -7,9 +7,7 @@ export class WikiService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll(filters: PaginationDto) {
|
||||
const { search, page = 1, limit = 10, sortBy: rawSortBy = 'term', sortOrder = 'asc' } = filters;
|
||||
const allowedSortFields = ['key', 'term', 'wikiId'];
|
||||
const sortBy = allowedSortFields.includes(rawSortBy) ? rawSortBy : 'term';
|
||||
const { search, page = 1, limit = 10, sortBy = 'term', sortOrder = 'asc' } = filters;
|
||||
|
||||
const whereClause: any = {};
|
||||
if (search) {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5050 --host 127.0.0.1",
|
||||
"build": "vite build",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
|
||||
@ -186,7 +186,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{mediaList.map((media) => {
|
||||
const imgUrl = media.url.startsWith('http') ? media.url : `https://apicanina.parsaaghayi.ir${media.url}`;
|
||||
const imgUrl = media.url.startsWith('http') ? media.url : `http://localhost:4001${media.url}`;
|
||||
const isSelected = selectedUrl && imgUrl === selectedUrl;
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -152,7 +152,7 @@ export default function Blogs() {
|
||||
<tr key={blog.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-3 px-6">
|
||||
{blog.imageUrl ? (
|
||||
<img src={blog.imageUrl.startsWith('http') ? blog.imageUrl : `https://apicanina.parsaaghayi.ir${blog.imageUrl}`} alt="blog" className="w-16 h-10 rounded-lg object-cover border border-gray-200" />
|
||||
<img src={blog.imageUrl.startsWith('http') ? blog.imageUrl : `http://localhost:4001${blog.imageUrl}`} alt="blog" className="w-16 h-10 rounded-lg object-cover border border-gray-200" />
|
||||
) : (
|
||||
<div className="w-16 h-10 rounded-lg bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
@ -222,7 +222,7 @@ export default function Blogs() {
|
||||
<label className="text-sm font-bold text-gray-700">تصویر شاخص</label>
|
||||
<div className="flex items-center gap-4">
|
||||
{formData.imageUrl ? (
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `https://apicanina.parsaaghayi.ir${formData.imageUrl}`} alt="preview" className="w-24 h-16 rounded-xl object-cover border border-gray-200" />
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `http://localhost:4001${formData.imageUrl}`} alt="preview" className="w-24 h-16 rounded-xl object-cover border border-gray-200" />
|
||||
) : (
|
||||
<div className="w-24 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
|
||||
@ -158,7 +158,7 @@ export default function Categories() {
|
||||
<tr key={cat.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-3 px-6">
|
||||
{cat.imageUrl ? (
|
||||
<img src={cat.imageUrl.startsWith('http') ? cat.imageUrl : `https://apicanina.parsaaghayi.ir${cat.imageUrl}`} alt={cat.name} className="w-12 h-12 rounded-xl object-cover border border-gray-200" />
|
||||
<img src={cat.imageUrl.startsWith('http') ? cat.imageUrl : `http://localhost:4001${cat.imageUrl}`} alt={cat.name} className="w-12 h-12 rounded-xl object-cover border border-gray-200" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
@ -240,7 +240,7 @@ export default function Categories() {
|
||||
<label className="text-sm font-bold text-gray-700">تصویر دستهبندی</label>
|
||||
<div className="flex items-center gap-4">
|
||||
{formData.imageUrl ? (
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `https://apicanina.parsaaghayi.ir${formData.imageUrl}`} alt="preview" className="w-16 h-16 rounded-xl object-cover border border-gray-200" />
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `http://localhost:4001${formData.imageUrl}`} alt="preview" className="w-16 h-16 rounded-xl object-cover border border-gray-200" />
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
|
||||
@ -102,7 +102,7 @@ export default function MediaManager() {
|
||||
};
|
||||
|
||||
const handleCopyUrl = async (media: Media) => {
|
||||
const fullUrl = media.url.startsWith('http') ? media.url : `https://apicanina.parsaaghayi.ir${media.url}`;
|
||||
const fullUrl = media.url.startsWith('http') ? media.url : `http://localhost:4001${media.url}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullUrl);
|
||||
setCopiedId(media.id);
|
||||
@ -202,7 +202,7 @@ export default function MediaManager() {
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-6">
|
||||
{filtered.map(media => {
|
||||
const imgUrl = media.url.startsWith('http') ? media.url : `https://apicanina.parsaaghayi.ir${media.url}`;
|
||||
const imgUrl = media.url.startsWith('http') ? media.url : `http://localhost:4001${media.url}`;
|
||||
return (
|
||||
<div
|
||||
key={media.id}
|
||||
|
||||
@ -91,7 +91,7 @@ export default function Pets() {
|
||||
<tr key={pet.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-3 px-6">
|
||||
{pet.imageUrl ? (
|
||||
<img src={pet.imageUrl.startsWith('http') ? pet.imageUrl : `https://apicanina.parsaaghayi.ir${pet.imageUrl}`} alt={pet.name} className="w-12 h-12 rounded-full object-cover border-2 border-purple-100" />
|
||||
<img src={pet.imageUrl.startsWith('http') ? pet.imageUrl : `http://localhost:4001${pet.imageUrl}`} alt={pet.name} className="w-12 h-12 rounded-full object-cover border-2 border-purple-100" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center text-purple-300 border-2 border-purple-100">
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
|
||||
@ -214,7 +214,7 @@ export default function Products() {
|
||||
<tr key={product.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-3 px-6">
|
||||
{product.imageUrl ? (
|
||||
<img src={product.imageUrl.startsWith('http') ? product.imageUrl : `https://apicanina.parsaaghayi.ir${product.imageUrl}`} alt={product.name} className="w-12 h-12 rounded-xl object-cover border border-gray-200" />
|
||||
<img src={product.imageUrl.startsWith('http') ? product.imageUrl : `http://localhost:4001${product.imageUrl}`} alt={product.name} className="w-12 h-12 rounded-xl object-cover border border-gray-200" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
@ -385,7 +385,7 @@ export default function Products() {
|
||||
{formData.imageUrl ? (
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<div className="relative group">
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `https://apicanina.parsaaghayi.ir${formData.imageUrl}`} alt="product" className="max-w-xs h-48 object-cover rounded-xl shadow-md" />
|
||||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `http://localhost:4001${formData.imageUrl}`} alt="product" className="max-w-xs h-48 object-cover rounded-xl shadow-md" />
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex items-center justify-center gap-3">
|
||||
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-white text-gray-800 px-4 py-2 rounded-lg font-bold text-sm">تغییر عکس</button>
|
||||
</div>
|
||||
@ -435,7 +435,7 @@ export default function Products() {
|
||||
isOpen={isMediaSelectorOpen}
|
||||
onClose={() => setIsMediaSelectorOpen(false)}
|
||||
onSelect={(url) => setFormData({...formData, imageUrl: url})}
|
||||
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `https://apicanina.parsaaghayi.ir${formData.imageUrl}` : undefined}
|
||||
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `http://localhost:4001${formData.imageUrl}` : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'https://apicanina.parsaaghayi.ir/api', // Backend NestJS API URL
|
||||
baseURL: 'http://localhost:4001/api', // Backend NestJS API URL
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@ -3,11 +3,9 @@ import { ChevronRight, Calendar, User, Sparkles } from "lucide-react";
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'https://apicanina.parsaaghayi.ir';
|
||||
|
||||
async function getBlog(slug: string) {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/blogs/${slug}`, { next: { revalidate: 60 } });
|
||||
const res = await fetch(`http://localhost:4001/api/blogs/${slug}`, { next: { revalidate: 60 } });
|
||||
if (!res.ok) throw new Error('Failed to fetch blog');
|
||||
const b = await res.json();
|
||||
return {
|
||||
@ -56,6 +54,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
|
||||
return (
|
||||
<div className="min-h-screen bg-medical-gray-50 pt-8 pb-20 px-4 font-vazir" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Breadcrumbs */}
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
|
||||
<Link href="/" className="hover:text-canina-blue transition-colors">خانه</Link>
|
||||
<ChevronRight className="w-2.5 h-2.5" />
|
||||
|
||||
@ -7,7 +7,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
async function getBlogs() {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'https://apicanina.parsaaghayi.ir';
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001';
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/api/blogs`, { next: { revalidate: 60 } });
|
||||
if (!res.ok) throw new Error('Failed to fetch blogs');
|
||||
|
||||
@ -2,12 +2,10 @@ import Link from 'next/link';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'https://apicanina.parsaaghayi.ir';
|
||||
|
||||
async function getWikiTerm(key: string) {
|
||||
async function getBlog(slug: string) {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/wiki/${encodeURIComponent(key)}`, { next: { revalidate: 60 } });
|
||||
if (!res.ok) throw new Error('Failed to fetch wiki term');
|
||||
const res = await fetch(`http://localhost:4001/api/blogs/${slug}`, { next: { revalidate: 60 } });
|
||||
if (!res.ok) throw new Error('Failed to fetch blog');
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -17,23 +15,23 @@ async function getWikiTerm(key: string) {
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const term = await getWikiTerm(slug);
|
||||
if (!term) return { title: 'ترکیب علمی یافت نشد' };
|
||||
|
||||
const blog = await getBlog(slug);
|
||||
if (!blog) return { title: 'مقاله یافت نشد' };
|
||||
|
||||
return {
|
||||
title: term.metaTitle || term.term,
|
||||
description: term.metaDescription || term.definition?.substring(0, 160),
|
||||
title: blog.metaTitle || blog.title,
|
||||
description: blog.metaDescription || blog.title,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function WikiTermPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
export default async function BlogPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const term = await getWikiTerm(slug);
|
||||
const blog = await getBlog(slug);
|
||||
|
||||
if (!term) {
|
||||
if (!blog) {
|
||||
return (
|
||||
<div className="min-h-[50vh] flex flex-col items-center justify-center font-vazir text-center" dir="rtl">
|
||||
<h1 className="text-3xl font-black text-medical-gray-900 mb-4">ترکیب علمی یافت نشد</h1>
|
||||
<h1 className="text-3xl font-black text-medical-gray-900 mb-4">مقالهای یافت نشد</h1>
|
||||
<Link href="/wiki" className="text-canina-blue font-bold flex items-center gap-2">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
بازگشت به دانشنامه
|
||||
@ -47,16 +45,32 @@ export default async function WikiTermPage({ params }: { params: Promise<{ slug:
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Link href="/wiki" className="inline-flex items-center gap-2 text-medical-gray-400 hover:text-canina-blue transition-colors font-bold mb-8">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
بازگشت به دانشنامه
|
||||
بازگشت به مقالات
|
||||
</Link>
|
||||
|
||||
{blog.imageUrl && (
|
||||
<div className="aspect-video rounded-[2rem] overflow-hidden mb-12 shadow-xl border border-medical-gray-100">
|
||||
<img src={blog.imageUrl} alt={blog.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-4xl lg:text-5xl font-black text-medical-gray-900 mb-6 leading-tight">
|
||||
{term.term}
|
||||
{blog.title}
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm font-bold text-medical-gray-400 mb-12 pb-8 border-b border-medical-gray-100">
|
||||
<span>نوشته شده در {new Date(blog.createdAt).toLocaleDateString('fa-IR')}</span>
|
||||
{blog.author && (
|
||||
<>
|
||||
<span className="w-1 h-1 rounded-full bg-medical-gray-300" />
|
||||
<span>توسط {blog.author.firstName} {blog.author.lastName}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="prose prose-lg prose-medical-gray max-w-none prose-headings:font-black prose-p:leading-relaxed prose-a:text-canina-blue"
|
||||
dangerouslySetInnerHTML={{ __html: term.definition }}
|
||||
dangerouslySetInnerHTML={{ __html: blog.content }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -65,8 +65,11 @@ import { useRouter } from 'next/navigation';
|
||||
export default function Hero({ banners = [] }: { banners?: any[] }) {
|
||||
const router = useRouter();
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
const title = getText('hero_title', '') || (banners.length > 0 ? banners[0].title : "تخصص آلمانی در خدمت\nسلامت پتهای خانگی");
|
||||
const subtitle = getText('hero_desc', '') || (banners.length > 0 ? banners[0].subtitle : 'بیش از <span className="font-bold text-canina-blue">۴۰ سال</span> تجربه نوآورانه در تولید مکملهای درمانی با بالاترین استاندارد کیفی «گرید دارویی اختصاصی». راهکار هوشمند برای هر نیاز بالینی.');
|
||||
const title = banners.length > 0 ? banners[0].title : getText('hero_title', "تخصص آلمانی در خدمت\nسلامت پتهای خانگی");
|
||||
const subtitle = banners.length > 0 ? banners[0].subtitle : getText(
|
||||
'hero_desc',
|
||||
'بیش از <span className="font-bold text-canina-blue">۴۰ سال</span> تجربه نوآورانه در تولید مکملهای درمانی با بالاترین استاندارد کیفی «گرید دارویی اختصاصی». راهکار هوشمند برای هر نیاز بالینی.'
|
||||
);
|
||||
const titleParts = title.split('\n');
|
||||
|
||||
return (
|
||||
|
||||
@ -11,7 +11,6 @@ import { useRouter } from 'next/navigation';
|
||||
export default function IngredientWiki() {
|
||||
const router = useRouter();
|
||||
const texts = useSettingsStore(state => state.texts);
|
||||
const scientificTerms = useSettingsStore(state => state.scientificTerms);
|
||||
const [ingredientsWiki, setIngredientsWiki] = useState<IngredientInfo[]>([]);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@ -32,31 +31,12 @@ export default function IngredientWiki() {
|
||||
description: item.description,
|
||||
benefits: Array.isArray(item.benefits) ? item.benefits : []
|
||||
})));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ingredients_wiki:", e);
|
||||
}
|
||||
}
|
||||
const terms = Object.values(scientificTerms);
|
||||
if (terms.length > 0) {
|
||||
setIngredientsWiki(terms.map((t: any) => {
|
||||
let benefits: string[] = [];
|
||||
if (t.keywords) {
|
||||
try {
|
||||
const parsed = typeof t.keywords === 'string' ? JSON.parse(t.keywords) : t.keywords;
|
||||
if (Array.isArray(parsed)) benefits = parsed;
|
||||
} catch (e) {}
|
||||
}
|
||||
return {
|
||||
id: t.key,
|
||||
name: t.term,
|
||||
description: t.definition,
|
||||
benefits
|
||||
};
|
||||
}));
|
||||
}
|
||||
}, [texts, scientificTerms]);
|
||||
}, [texts]);
|
||||
|
||||
return (
|
||||
<div className="bg-white py-12 px-4 overflow-hidden font-vazir" dir="rtl">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const baseURL = typeof window !== 'undefined' ? '/api' : (process.env.NEXT_PUBLIC_API_URL || 'https://apicanina.parsaaghayi.ir/api');
|
||||
const baseURL = typeof window !== 'undefined' ? '/api' : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api');
|
||||
|
||||
const api = axios.create({
|
||||
baseURL,
|
||||
|
||||
@ -56,7 +56,7 @@ export class ProductService {
|
||||
onSetOfAction: data.onSetOfAction,
|
||||
optimisticTemplate: data.optimisticTemplate,
|
||||
feedingAdvice: data.feedingAdvice,
|
||||
image: data.imageUrl?.startsWith('http') ? data.imageUrl : data.imageUrl ? `/api${data.imageUrl}` : data.image,
|
||||
image: data.imageUrl?.startsWith('http') ? data.imageUrl : data.imageUrl ? `http://localhost:4001${data.imageUrl}` : data.image,
|
||||
|
||||
main_ingredients: data.ingredientList?.map((i: any) => i.ingredient) || data.ingredients?.split(/[،,-]/).map((s: string) => s.trim()).filter(Boolean) || [],
|
||||
symptoms: data.symptoms?.map((s: any) => s.symptom || s) || [],
|
||||
|
||||
26
nginx.conf
26
nginx.conf
@ -1,15 +1,17 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name canina.parsaaghayi.ir;
|
||||
listen 8080 default_server;
|
||||
server_name _;
|
||||
root /app;
|
||||
index index.html;
|
||||
|
||||
# Security Headers
|
||||
server_tokens off;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Frame-ों "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval' https://images.unsplash.com https://canina.de;" always;
|
||||
|
||||
# Main Application (Next.js)
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
@ -18,22 +20,11 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Backend API
|
||||
location /api {
|
||||
proxy_pass http://backend_prod:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name admincanina.parsaaghayi.ir;
|
||||
|
||||
root /app/admin_static;
|
||||
index index.html;
|
||||
|
||||
@ -43,15 +34,14 @@ server {
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval' https://images.unsplash.com https://canina.de;" always;
|
||||
|
||||
# SPA routing for Admin Panel
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Backend API for Admin
|
||||
location /api {
|
||||
proxy_pass http://backend_prod:3000;
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user