feat: add Technogen reports section with PDF downloads and CMS management
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
|
@ -0,0 +1,105 @@
|
|||
export type TechnogenReport = {
|
||||
id: string
|
||||
code?: string
|
||||
tagFa?: string
|
||||
tagEn?: string
|
||||
titleFa: string
|
||||
titleEn?: string
|
||||
subtitleFa?: string
|
||||
subtitleEn?: string
|
||||
descriptionFa?: string
|
||||
descriptionEn?: string
|
||||
dateFa?: string
|
||||
dateEn?: string
|
||||
pagesFa?: string
|
||||
pagesEn?: string
|
||||
isFeatured?: boolean
|
||||
coverImage?: string
|
||||
fileUrl?: string
|
||||
sortOrder?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export const FALLBACK_TECHNOGEN_REPORTS: TechnogenReport[] = [
|
||||
{
|
||||
id: 'tgn-cs-01',
|
||||
code: 'TGN-CS-01',
|
||||
tagFa: 'مطالعه تطبیقی',
|
||||
tagEn: 'Comparative Study',
|
||||
titleFa: 'پوسکو؛ از فولادساز ملی تا توسعهگر فناوری',
|
||||
titleEn: 'POSCO: From National Steelmaker to Technology Developer',
|
||||
subtitleFa: 'چگونه یک بنگاه بدون سرمایه، فناوری و تجربه، در پنج دهه به مالک، توسعهدهنده و صادرکننده فناوری تبدیل شد',
|
||||
subtitleEn: 'How a firm with zero capital, technology, or experience became an owner, developer, and exporter of steel technology across five decades.',
|
||||
descriptionFa: 'تحلیل عمیق ۵ دهه یادگیری فناورانه، سرمایهگذاری استراتژیک در تحقیق و توسعه، جهش تکنولوژیک و ساخت اکوسیستم نوآوری با مشارکت دانشگاه پوهانگ (POSTECH) و موسسه RIST.',
|
||||
descriptionEn: 'In-depth analysis of five decades of technological learning, strategic R&D investment, and innovation ecosystem building.',
|
||||
dateFa: 'مرداد ۱۴۰۵',
|
||||
dateEn: 'August 2026',
|
||||
pagesFa: '۳۰ صفحه',
|
||||
pagesEn: '30 Pages',
|
||||
isFeatured: true,
|
||||
coverImage: '/technogen/technogen_posco_cover.png',
|
||||
fileUrl: '/technogen/technogen_posco_study.pdf',
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'tgn-an-01',
|
||||
code: 'TGN-AN-01',
|
||||
tagFa: 'یادداشت تحلیلی',
|
||||
tagEn: 'Analytical Note',
|
||||
titleFa: 'از تولیدکننده تا صاحب فناوری؛ مسیر جهش صنعت ایران',
|
||||
titleEn: 'From Manufacturer to Technology Owner: Iran\'s Industrial Leap Pathway',
|
||||
subtitleFa: 'درسهای یادگیری صنعتی در کرهجنوبی و برزیل برای ساخت قابلیت فناورانه در ایران',
|
||||
subtitleEn: 'Lessons from industrial learning in South Korea and Brazil for building technological capabilities in Iran.',
|
||||
descriptionFa: 'چرا تمرکز صرف بر ظرفیت تولید بدون ساخت قابلیت درونزای طراحی و مهندسی، صنعت را در دام مونتاژ و وابستگی به لایسنسور خارجی نگه میدارد.',
|
||||
descriptionEn: 'Why focusing solely on production volume without indigenous engineering keeps industry trapped in dependency.',
|
||||
dateFa: 'مرداد ۱۴۰۵',
|
||||
dateEn: 'August 2026',
|
||||
pagesFa: '۴ صفحه',
|
||||
pagesEn: '4 Pages',
|
||||
isFeatured: false,
|
||||
coverImage: '/technogen/tgn_an_01_cover.png',
|
||||
fileUrl: '/technogen/tgn_an_01.pdf',
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'tgn-an-02',
|
||||
code: 'TGN-AN-02',
|
||||
tagFa: 'یادداشت تحلیلی',
|
||||
tagEn: 'Analytical Note',
|
||||
titleFa: 'رشد بدون توسعه؛ چرا صنعتیشدن بدون فناورشدن به سقف میخورد؟',
|
||||
titleEn: 'Growth Without Development: Why Industrialization Without Technology Hits a Ceiling',
|
||||
subtitleFa: 'بهرهوری سقف دارد؛ فناوری ندارد. چرا انباشت قابلیت فناورانه، شرط عبور از سقف رشد صنعتی است.',
|
||||
subtitleEn: 'Productivity has a ceiling; technology does not. Why technological capability accumulation is essential for surpassing industrial growth limits.',
|
||||
descriptionFa: 'بررسی منحنیهای مرز کارایی و تحلیل تفاوت بنیادی میان بهینهسازی عملیاتی موجود و جابهجایی مرز فناوری از طریق خلق دانش فنی.',
|
||||
descriptionEn: 'Analyzing efficiency frontiers and the fundamental difference between operational optimization and technological boundary shifts.',
|
||||
dateFa: 'مرداد ۱۴۰۵',
|
||||
dateEn: 'August 2026',
|
||||
pagesFa: '۴ صفحه',
|
||||
pagesEn: '4 Pages',
|
||||
isFeatured: false,
|
||||
coverImage: '/technogen/tgn_an_02_cover.png',
|
||||
fileUrl: '/technogen/tgn_an_02.pdf',
|
||||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
id: 'tgn-an-03',
|
||||
code: 'TGN-AN-03',
|
||||
tagFa: 'یادداشت تحلیلی',
|
||||
tagEn: 'Analytical Note',
|
||||
titleFa: 'توسعهگر فناوری یا مصرفکنندهٔ دائمی؟ انتخابی که بنگاههای بزرگ باید امروز بکنند',
|
||||
titleEn: 'Technology Developer or Perpetual Consumer? The Critical Choice for Large Enterprises Today',
|
||||
subtitleFa: 'درسهای جهش و میانبر فناورانه از بنگاههای بزرگ کشورهای در حال توسعه؛ از پوسکو و امبرائر تا سیمکس و بائواستیل',
|
||||
subtitleEn: 'Technological leapfrogging lessons from large emerging-market enterprises: from POSCO and Embraer to CEMEX and Baosteel.',
|
||||
descriptionFa: 'تفاوت در مقصد سرمایهگذاری: یکی برای «داشتن» فناوری خرج میکند و دیگری برای «توانستن» — یعنی جذب، تغییر، طراحی و بازآفرینی فناوری.',
|
||||
descriptionEn: 'The difference in investment purpose: spending to acquire technology versus spending to build the capability to absorb, modify, design, and recreate it.',
|
||||
dateFa: 'مرداد ۱۴۰۵',
|
||||
dateEn: 'August 2026',
|
||||
pagesFa: '۴ صفحه',
|
||||
pagesEn: '4 Pages',
|
||||
isFeatured: false,
|
||||
coverImage: '/technogen/tgn_an_03_cover.png',
|
||||
fileUrl: '/technogen/tgn_an_03.pdf',
|
||||
sortOrder: 4,
|
||||
},
|
||||
]
|
||||
|
|
@ -3,6 +3,7 @@ import IntegrationsSection from './sections/IntegrationsSection'
|
|||
// import InternalNewsSection from './sections/InternalNewsSection' // «تازهترین اخبار صنعت» — disabled, restore anytime
|
||||
import RadarTechSection from './sections/RadarTechSection'
|
||||
import AtAGlanceSection from './sections/AtAGlanceSection'
|
||||
import TechnogenSection from './sections/TechnogenSection'
|
||||
import FeaturedReportBanner from './sections/FeaturedReportBanner'
|
||||
import MapEventsSection from './sections/MapEventsSection'
|
||||
import FactoryReportsScroll from './sections/FactoryReportsScroll'
|
||||
|
|
@ -19,6 +20,7 @@ export default function Home() {
|
|||
<RadarTechSection only="radar" /> {/* رادار آینده */}
|
||||
<IntegrationsSection /> {/* آمار و داده — نبض صنعت */}
|
||||
<AtAGlanceSection /> {/* در یک نگاه */}
|
||||
<TechnogenSection /> {/* تکنوژن؛ برنامه توسعهگر ملی فناوری */}
|
||||
<MapEventsSection /> {/* دیدهبان رویدادها */}
|
||||
<FeaturedReportBanner /> {/* دیپلماسی (گزارش ویژه) */}
|
||||
<FactoryReportsScroll /> {/* ماهنامه تحلیلی کارخانه */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,854 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Download, FileText, ExternalLink, Sparkles, X, Eye, BookOpen, Layers } from 'lucide-react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { FALLBACK_TECHNOGEN_REPORTS, type TechnogenReport } from '@/data/technogen'
|
||||
|
||||
const NAVY = '#032340'
|
||||
const GOLD = '#CD9E53'
|
||||
const EASE = [0.22, 1, 0.36, 1] as const
|
||||
|
||||
export default function TechnogenSection() {
|
||||
const { lang } = useLang()
|
||||
const isFa = lang === 'fa'
|
||||
const [reports, setReports] = useState<TechnogenReport[]>(FALLBACK_TECHNOGEN_REPORTS)
|
||||
const [selectedReport, setSelectedReport] = useState<TechnogenReport | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false
|
||||
fetch('/api/technogen')
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('API error')
|
||||
return res.json()
|
||||
})
|
||||
.then((data: TechnogenReport[]) => {
|
||||
if (!unmounted && Array.isArray(data) && data.length > 0) {
|
||||
setReports(data)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// use fallback reports seamlessly
|
||||
})
|
||||
return () => {
|
||||
unmounted = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Featured report (POSCO study) vs analytical notes
|
||||
const featured = reports.find((r) => r.isFeatured) || reports[0]
|
||||
const analyticalNotes = reports.filter((r) => r.id !== featured?.id)
|
||||
|
||||
const openPreview = (report: TechnogenReport) => {
|
||||
setSelectedReport(report)
|
||||
}
|
||||
|
||||
const closePreview = () => {
|
||||
setSelectedReport(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="technogen"
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
style={{
|
||||
background: 'var(--paper)',
|
||||
borderTop: '1px solid var(--rule-thin)',
|
||||
borderBottom: '1px solid var(--rule-thin)',
|
||||
padding: '72px 0 88px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Background ambient lighting */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-15%',
|
||||
right: isFa ? '-5%' : 'auto',
|
||||
left: isFa ? 'auto' : '-5%',
|
||||
width: '550px',
|
||||
height: '550px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(205,158,83,0.08) 0%, rgba(3,35,64,0) 70%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-20%',
|
||||
left: isFa ? '-10%' : 'auto',
|
||||
right: isFa ? 'auto' : '-10%',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(0,174,239,0.06) 0%, rgba(3,35,64,0) 70%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||
{/* ── Section Header ── */}
|
||||
<div style={{ marginBottom: 40, position: 'relative', zIndex: 1 }}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, ease: EASE }}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, marginBottom: 12 }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '4px 12px',
|
||||
borderRadius: 20,
|
||||
background: 'rgba(3,35,64,0.06)',
|
||||
border: '1px solid rgba(3,35,64,0.12)',
|
||||
color: NAVY,
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
letterSpacing: isFa ? '0' : '1px',
|
||||
}}
|
||||
>
|
||||
<Sparkles size={14} color={GOLD} />
|
||||
TECHNOGEN
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: GOLD, fontWeight: 700 }}>
|
||||
{isFa ? 'برنامه توسعهگر ملی فناوری' : 'National Technology Developer Program'}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', flexWrap: 'wrap', gap: 16 }}>
|
||||
<div>
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, ease: EASE, delay: 0.1 }}
|
||||
style={{
|
||||
fontSize: 'clamp(24px, 2.8vw, 36px)',
|
||||
fontWeight: 900,
|
||||
color: NAVY,
|
||||
margin: '0 0 8px',
|
||||
letterSpacing: isFa ? '-0.5px' : '-0.2px',
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{isFa ? 'تکنوژن؛ برنامه توسعهگر ملی فناوری' : 'Technogen: National Technology Developer Program'}
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, ease: EASE, delay: 0.15 }}
|
||||
style={{
|
||||
fontSize: 'clamp(14px, 1.2vw, 16px)',
|
||||
color: 'rgba(3,35,64,0.7)',
|
||||
margin: 0,
|
||||
maxWidth: 720,
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
{isFa
|
||||
? 'گزارشهای تحلیلی و مطالعات تطبیقی یادگیری صنعتی، انتقال دانش و ساخت قابلیت فناورانه در زنجیره فولاد'
|
||||
: 'Analytical notes and comparative studies on industrial learning, knowledge absorption, and technological capability building in steel.'}
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Flagship Featured Report (POSCO Study) ── */}
|
||||
{featured && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: EASE }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(135deg, ${NAVY} 0%, #062b4f 50%, #041c33 100%)`,
|
||||
boxShadow: '0 25px 50px -18px rgba(3,35,64,0.35)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
marginBottom: 36,
|
||||
}}
|
||||
>
|
||||
{/* Ambient decorative elements */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundImage: 'radial-gradient(circle at 85% 20%, rgba(205,158,83,0.15) 0%, transparent 50%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 380,
|
||||
padding: '36px 44px',
|
||||
}}
|
||||
className="max-lg:!flex-col max-md:!p-6"
|
||||
>
|
||||
{/* Cover Column */}
|
||||
<div
|
||||
style={{
|
||||
flex: '0 0 280px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
className="max-lg:!w-full max-lg:!mb-8"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.03, rotateY: isFa ? -4 : 4, y: -4 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 20px 40px -10px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.12)',
|
||||
overflow: 'hidden',
|
||||
background: NAVY,
|
||||
maxWidth: 240,
|
||||
width: '100%',
|
||||
aspectRatio: '1 / 1.414',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => openPreview(featured)}
|
||||
>
|
||||
<img
|
||||
src={featured.coverImage}
|
||||
alt={isFa ? featured.titleFa : (featured.titleEn || featured.titleFa)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(to top, rgba(3,35,64,0.6) 0%, transparent 40%)',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
padding: 12,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.25s',
|
||||
}}
|
||||
className="hover:!opacity-100"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.95)',
|
||||
color: NAVY,
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
padding: '6px 14px',
|
||||
borderRadius: 20,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Eye size={14} />
|
||||
{isFa ? 'مشاهده گزارش' : 'Preview'}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Text / Info Column */}
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
marginInlineStart: 36,
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
className="max-lg:!m-0 max-lg:!w-full max-lg:!text-center"
|
||||
>
|
||||
{/* Meta Badges */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 14,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
className="max-lg:!justify-center"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
background: GOLD,
|
||||
color: NAVY,
|
||||
fontSize: 11,
|
||||
fontWeight: 900,
|
||||
padding: '4px 10px',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
{isFa ? 'مطالعه تطبیقی ویژه' : 'Flagship Case Study'}
|
||||
</span>
|
||||
{featured.code && (
|
||||
<span
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.12)',
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
padding: '4px 10px',
|
||||
borderRadius: 6,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
{featured.code}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', fontWeight: 600 }}>
|
||||
{isFa ? (featured.pagesFa || '۳۰ صفحه') : (featured.pagesEn || '30 Pages')}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.3)' }}>•</span>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)' }}>
|
||||
{isFa ? (featured.dateFa || 'مرداد ۱۴۰۵') : (featured.dateEn || 'August 2026')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3
|
||||
style={{
|
||||
fontSize: 'clamp(20px, 2.3vw, 28px)',
|
||||
fontWeight: 900,
|
||||
lineHeight: 1.4,
|
||||
margin: '0 0 12px',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{isFa ? featured.titleFa : (featured.titleEn || featured.titleFa)}
|
||||
</h3>
|
||||
|
||||
{/* Subtitle / Quote */}
|
||||
{featured.subtitleFa && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 'clamp(14px, 1.1vw, 16px)',
|
||||
lineHeight: 1.7,
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontWeight: 600,
|
||||
marginBottom: 14,
|
||||
borderInlineStart: `3px solid ${GOLD}`,
|
||||
paddingInlineStart: 14,
|
||||
}}
|
||||
className="max-lg:!border-none max-lg:!p-0"
|
||||
>
|
||||
{isFa ? featured.subtitleFa : (featured.subtitleEn || featured.subtitleFa)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{featured.descriptionFa && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: 13,
|
||||
lineHeight: 1.8,
|
||||
color: 'rgba(255,255,255,0.65)',
|
||||
margin: '0 0 24px',
|
||||
maxWidth: 680,
|
||||
}}
|
||||
className="max-lg:!mx-auto"
|
||||
>
|
||||
{isFa ? featured.descriptionFa : (featured.descriptionEn || featured.descriptionFa)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
className="max-lg:!justify-center"
|
||||
>
|
||||
{featured.fileUrl && (
|
||||
<a
|
||||
href={featured.fileUrl}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
background: `linear-gradient(135deg, ${GOLD} 0%, #e0b469 100%)`,
|
||||
color: NAVY,
|
||||
fontSize: 13,
|
||||
fontWeight: 900,
|
||||
padding: '12px 24px',
|
||||
borderRadius: 12,
|
||||
textDecoration: 'none',
|
||||
boxShadow: '0 10px 25px -5px rgba(205,158,83,0.4)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
className="hover:!brightness-105 active:!scale-95"
|
||||
>
|
||||
<Download size={17} strokeWidth={2.5} />
|
||||
<span>{isFa ? 'دانلود متن کامل گزارش (PDF)' : 'Download Full PDF (30 Pages)'}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => openPreview(featured)}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
padding: '12px 20px',
|
||||
borderRadius: 12,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
className="hover:!bg-white/15"
|
||||
>
|
||||
<BookOpen size={16} />
|
||||
<span>{isFa ? 'مطالعه و ورق زدن آنلاین' : 'Read Online'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* ── Analytical Notes (3-Card Grid) ── */}
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Layers size={18} color={NAVY} />
|
||||
<h4 style={{ fontSize: 18, fontWeight: 900, color: NAVY, margin: 0 }}>
|
||||
{isFa ? 'یادداشتهای تحلیلی تکنوژن' : 'Technogen Analytical Notes'}
|
||||
</h4>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: 'rgba(3,35,64,0.6)', fontWeight: 600 }}>
|
||||
{isFa ? `${analyticalNotes.length} شماره منتشر شده` : `${analyticalNotes.length} Published Notes`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: 20,
|
||||
}}
|
||||
className="max-lg:!grid-cols-2 max-md:!grid-cols-1"
|
||||
>
|
||||
{analyticalNotes.map((note, idx) => (
|
||||
<motion.article
|
||||
key={note.id}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, ease: EASE, delay: idx * 0.1 }}
|
||||
whileHover={{ y: -6 }}
|
||||
style={{
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
border: '1px solid rgba(3,35,64,0.08)',
|
||||
boxShadow: '0 8px 24px -6px rgba(3,35,64,0.06)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
}}
|
||||
className="hover:!shadow-xl"
|
||||
>
|
||||
{/* Card Top / Cover & Tag */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: `linear-gradient(135deg, ${NAVY} 0%, #0a2f52 100%)`,
|
||||
padding: '24px 20px 20px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => openPreview(note)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 14,
|
||||
right: isFa ? 14 : 'auto',
|
||||
left: isFa ? 'auto' : 14,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{note.code && (
|
||||
<span
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
padding: '3px 8px',
|
||||
borderRadius: 4,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
{note.code}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
background: 'rgba(205,158,83,0.25)',
|
||||
color: GOLD,
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
padding: '3px 8px',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{isFa ? (note.tagFa || 'یادداشت تحلیلی') : (note.tagEn || 'Analytical Note')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Cover Image */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
aspectRatio: '1 / 1.414',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
marginTop: 18,
|
||||
boxShadow: '0 12px 24px -4px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
background: NAVY,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={note.coverImage}
|
||||
alt={isFa ? note.titleFa : (note.titleEn || note.titleFa)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Body */}
|
||||
<div
|
||||
style={{
|
||||
padding: '20px 22px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 auto',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(3,35,64,0.55)',
|
||||
fontWeight: 700,
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span>{isFa ? (note.dateFa || 'مرداد ۱۴۰۵') : (note.dateEn || 'August 2026')}</span>
|
||||
<span>{isFa ? (note.pagesFa || '۴ صفحه') : (note.pagesEn || '4 Pages')}</span>
|
||||
</div>
|
||||
|
||||
<h4
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 900,
|
||||
color: NAVY,
|
||||
lineHeight: 1.45,
|
||||
margin: '0 0 8px',
|
||||
minHeight: 46,
|
||||
}}
|
||||
>
|
||||
{isFa ? note.titleFa : (note.titleEn || note.titleFa)}
|
||||
</h4>
|
||||
|
||||
{note.subtitleFa && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: 1.7,
|
||||
color: 'rgba(3,35,64,0.7)',
|
||||
margin: '0 0 16px',
|
||||
flex: '1 1 auto',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{isFa ? note.subtitleFa : (note.subtitleEn || note.subtitleFa)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingTop: 12,
|
||||
borderTop: '1px solid rgba(3,35,64,0.06)',
|
||||
marginTop: 'auto',
|
||||
}}
|
||||
>
|
||||
{note.fileUrl && (
|
||||
<a
|
||||
href={note.fileUrl}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
background: 'rgba(3,35,64,0.06)',
|
||||
color: NAVY,
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
textDecoration: 'none',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
className="hover:!bg-navy hover:!text-white"
|
||||
>
|
||||
<Download size={14} />
|
||||
<span>{isFa ? 'دانلود PDF' : 'Download'}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => openPreview(note)}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(3,35,64,0.15)',
|
||||
color: NAVY,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
className="hover:!border-navy"
|
||||
>
|
||||
<Eye size={14} />
|
||||
<span>{isFa ? 'مشاهده' : 'View'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Document Reader / Preview Modal ── */}
|
||||
<AnimatePresence>
|
||||
{selectedReport && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '24px 16px',
|
||||
}}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'rgba(3, 20, 36, 0.85)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
onClick={closePreview}
|
||||
/>
|
||||
|
||||
{/* Modal Window */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
maxWidth: 1000,
|
||||
height: '90vh',
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 24,
|
||||
boxShadow: '0 30px 90px rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Modal Top Header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 24px',
|
||||
background: NAVY,
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
|
||||
<FileText size={20} color={GOLD} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11, color: GOLD, fontWeight: 700 }}>
|
||||
{selectedReport.code} · {isFa ? selectedReport.tagFa : (selectedReport.tagEn || selectedReport.tagFa)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 900,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{isFa ? selectedReport.titleFa : (selectedReport.titleEn || selectedReport.titleFa)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
|
||||
{selectedReport.fileUrl && (
|
||||
<a
|
||||
href={selectedReport.fileUrl}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
background: GOLD,
|
||||
color: NAVY,
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
padding: '6px 14px',
|
||||
borderRadius: 8,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<Download size={14} />
|
||||
<span>{isFa ? 'دانلود فایل' : 'Download'}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedReport.fileUrl && (
|
||||
<a
|
||||
href={selectedReport.fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: '#fff',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
title={isFa ? 'باز کردن در پنجره جدید' : 'Open in new tab'}
|
||||
>
|
||||
<ExternalLink size={15} />
|
||||
</a>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={closePreview}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title={isFa ? 'بستن' : 'Close'}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Viewer Body */}
|
||||
<div style={{ flex: '1 1 0', position: 'relative', background: '#e5e7eb' }}>
|
||||
{selectedReport.fileUrl ? (
|
||||
<iframe
|
||||
src={`${selectedReport.fileUrl}#toolbar=1&navpanes=0`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
title={selectedReport.titleFa}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ display: 'grid', placeItems: 'center', height: '100%', color: '#6b7280' }}>
|
||||
{isFa ? 'فایل PDF در دسترس نیست.' : 'PDF file not available.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
153
panel/db.js
|
|
@ -333,6 +333,30 @@ await pool.query(`
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_factory_reports_sort ON factory_reports(sort_order, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS technogen_reports (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT,
|
||||
tag_fa TEXT,
|
||||
tag_en TEXT,
|
||||
title_fa TEXT NOT NULL,
|
||||
title_en TEXT,
|
||||
subtitle_fa TEXT,
|
||||
subtitle_en TEXT,
|
||||
description_fa TEXT,
|
||||
description_en TEXT,
|
||||
date_fa TEXT,
|
||||
date_en TEXT,
|
||||
pages_fa TEXT,
|
||||
pages_en TEXT,
|
||||
is_featured INTEGER DEFAULT 0,
|
||||
cover_image TEXT,
|
||||
file_url TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_technogen_sort ON technogen_reports(is_featured DESC, sort_order ASC, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integrations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
|
|
@ -491,6 +515,108 @@ if (ownerRows.length === 0) {
|
|||
await pool.query("UPDATE users SET role = 'owner' WHERE id = (SELECT id FROM users ORDER BY id LIMIT 1)");
|
||||
}
|
||||
|
||||
// seed technogen reports if table is empty
|
||||
const { rows: techRows } = await pool.query("SELECT COUNT(*)::int AS cnt FROM technogen_reports");
|
||||
if (techRows[0]?.cnt === 0) {
|
||||
const INITIAL_TECHNOGEN_REPORTS = [
|
||||
{
|
||||
id: 'tgn-cs-01',
|
||||
code: 'TGN-CS-01',
|
||||
tag_fa: 'مطالعه تطبیقی',
|
||||
tag_en: 'Comparative Study',
|
||||
title_fa: 'پوسکو؛ از فولادساز ملی تا توسعهگر فناوری',
|
||||
title_en: 'POSCO: From National Steelmaker to Technology Developer',
|
||||
subtitle_fa: 'چگونه یک بنگاه بدون سرمایه، فناوری و تجربه، در پنج دهه به مالک، توسعهدهنده و صادرکننده فناوری تبدیل شد',
|
||||
subtitle_en: 'How a firm with zero capital, technology, or experience became an owner, developer, and exporter of steel technology across five decades.',
|
||||
description_fa: 'تحلیل عمیق ۵ دهه یادگیری فناورانه، سرمایهگذاری استراتژیک در تحقیق و توسعه، جهش تکنولوژیک و ساخت اکوسیستم نوآوری با مشارکت دانشگاه پوهانگ (POSTECH) و موسسه RIST.',
|
||||
description_en: 'In-depth analysis of five decades of technological learning, strategic R&D investment, and innovation ecosystem building.',
|
||||
date_fa: 'مرداد ۱۴۰۵',
|
||||
date_en: 'August 2026',
|
||||
pages_fa: '۳۰ صفحه',
|
||||
pages_en: '30 Pages',
|
||||
is_featured: 1,
|
||||
cover_image: '/technogen/technogen_posco_cover.png',
|
||||
file_url: '/technogen/technogen_posco_study.pdf',
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
id: 'tgn-an-01',
|
||||
code: 'TGN-AN-01',
|
||||
tag_fa: 'یادداشت تحلیلی',
|
||||
tag_en: 'Analytical Note',
|
||||
title_fa: 'از تولیدکننده تا صاحب فناوری؛ مسیر جهش صنعت ایران',
|
||||
title_en: 'From Manufacturer to Technology Owner: Iran\'s Industrial Leap Pathway',
|
||||
subtitle_fa: 'درسهای یادگیری صنعتی در کرهجنوبی و برزیل برای ساخت قابلیت فناورانه در ایران',
|
||||
subtitle_en: 'Lessons from industrial learning in South Korea and Brazil for building technological capabilities in Iran.',
|
||||
description_fa: 'چرا تمرکز صرف بر ظرفیت تولید بدون ساخت قابلیت درونزای طراحی و مهندسی، صنعت را در دام مونتاژ و وابستگی به لایسنسور خارجی نگه میدارد.',
|
||||
description_en: 'Why focusing solely on production volume without indigenous engineering keeps industry trapped in dependency.',
|
||||
date_fa: 'مرداد ۱۴۰۵',
|
||||
date_en: 'August 2026',
|
||||
pages_fa: '۴ صفحه',
|
||||
pages_en: '4 Pages',
|
||||
is_featured: 0,
|
||||
cover_image: '/technogen/tgn_an_01_cover.png',
|
||||
file_url: '/technogen/tgn_an_01.pdf',
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
id: 'tgn-an-02',
|
||||
code: 'TGN-AN-02',
|
||||
tag_fa: 'یادداشت تحلیلی',
|
||||
tag_en: 'Analytical Note',
|
||||
title_fa: 'رشد بدون توسعه؛ چرا صنعتیشدن بدون فناورشدن به سقف میخورد؟',
|
||||
title_en: 'Growth Without Development: Why Industrialization Without Technology Hits a Ceiling',
|
||||
subtitle_fa: 'بهرهوری سقف دارد؛ فناوری ندارد. چرا انباشت قابلیت فناورانه، شرط عبور از سقف رشد صنعتی است.',
|
||||
subtitle_en: 'Productivity has a ceiling; technology does not. Why technological capability accumulation is essential for surpassing industrial growth limits.',
|
||||
description_fa: 'بررسی منحنیهای مرز کارایی و تحلیل تفاوت بنیادی میان بهینهسازی عملیاتی موجود و جابهجایی مرز فناوری از طریق خلق دانش فنی.',
|
||||
description_en: 'Analyzing efficiency frontiers and the fundamental difference between operational optimization and technological boundary shifts.',
|
||||
date_fa: 'مرداد ۱۴۰۵',
|
||||
date_en: 'August 2026',
|
||||
pages_fa: '۴ صفحه',
|
||||
pages_en: '4 Pages',
|
||||
is_featured: 0,
|
||||
cover_image: '/technogen/tgn_an_02_cover.png',
|
||||
file_url: '/technogen/tgn_an_02.pdf',
|
||||
sort_order: 3,
|
||||
},
|
||||
{
|
||||
id: 'tgn-an-03',
|
||||
code: 'TGN-AN-03',
|
||||
tag_fa: 'یادداشت تحلیلی',
|
||||
tag_en: 'Analytical Note',
|
||||
title_fa: 'توسعهگر فناوری یا مصرفکنندهٔ دائمی؟ انتخابی که بنگاههای بزرگ باید امروز بکنند',
|
||||
title_en: 'Technology Developer or Perpetual Consumer? The Critical Choice for Large Enterprises Today',
|
||||
subtitle_fa: 'درسهای جهش و میانبر فناورانه از بنگاههای بزرگ کشورهای در حال توسعه؛ از پوسکو و امبرائر تا سیمکس و بائواستیل',
|
||||
subtitle_en: 'Technological leapfrogging lessons from large emerging-market enterprises: from POSCO and Embraer to CEMEX and Baosteel.',
|
||||
description_fa: 'تفاوت در مقصد سرمایهگذاری: یکی برای «داشتن» فناوری خرج میکند و دیگری برای «توانستن» — یعنی جذب، تغییر، طراحی و بازآفرینی فناوری.',
|
||||
description_en: 'The difference in investment purpose: spending to acquire technology versus spending to build the capability to absorb, modify, design, and recreate it.',
|
||||
date_fa: 'مرداد ۱۴۰۵',
|
||||
date_en: 'August 2026',
|
||||
pages_fa: '۴ صفحه',
|
||||
pages_en: '4 Pages',
|
||||
is_featured: 0,
|
||||
cover_image: '/technogen/tgn_an_03_cover.png',
|
||||
file_url: '/technogen/tgn_an_03.pdf',
|
||||
sort_order: 4,
|
||||
},
|
||||
];
|
||||
for (const item of INITIAL_TECHNOGEN_REPORTS) {
|
||||
await pool.query(`
|
||||
INSERT INTO technogen_reports (
|
||||
id, code, tag_fa, tag_en, title_fa, title_en, subtitle_fa, subtitle_en,
|
||||
description_fa, description_en, date_fa, date_en, pages_fa, pages_en,
|
||||
is_featured, cover_image, file_url, sort_order
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [
|
||||
item.id, item.code, item.tag_fa, item.tag_en, item.title_fa, item.title_en,
|
||||
item.subtitle_fa, item.subtitle_en, item.description_fa, item.description_en,
|
||||
item.date_fa, item.date_en, item.pages_fa, item.pages_en,
|
||||
item.is_featured, item.cover_image, item.file_url, item.sort_order,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeMediaUrl(url) {
|
||||
if (!url || typeof url !== 'string') return url;
|
||||
const match = url.match(/^(?:https?:\/\/[^\/]+)?\/(media|uploads)\/(.+)$/i);
|
||||
|
|
@ -850,3 +976,30 @@ export function rowToArticle(row) {
|
|||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export function rowToTechnogenReport(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
tagFa: row.tag_fa,
|
||||
tagEn: row.tag_en,
|
||||
titleFa: row.title_fa,
|
||||
titleEn: row.title_en,
|
||||
subtitleFa: row.subtitle_fa,
|
||||
subtitleEn: row.subtitle_en,
|
||||
descriptionFa: row.description_fa,
|
||||
descriptionEn: row.description_en,
|
||||
dateFa: row.date_fa,
|
||||
dateEn: row.date_en,
|
||||
pagesFa: row.pages_fa,
|
||||
pagesEn: row.pages_en,
|
||||
isFeatured: !!row.is_featured,
|
||||
coverImage: normalizeMediaUrl(row.cover_image),
|
||||
fileUrl: normalizeMediaUrl(row.file_url),
|
||||
sortOrder: row.sort_order,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ const $ = (sel, root = document) => root.querySelector(sel);
|
|||
const root = $('#app');
|
||||
|
||||
const TAB_GROUPS = [
|
||||
{ label: 'محتوا', tabs: ['glance', 'radarFuture', 'factoryReports', 'media', 'events', 'banners'] },
|
||||
{ label: 'محتوا', tabs: ['glance', 'radarFuture', 'technogen', 'factoryReports', 'media', 'events', 'banners'] },
|
||||
{ label: 'کاربران', tabs: ['members', 'newsletter'] },
|
||||
];
|
||||
const TAB_LABELS = {
|
||||
glance: 'در یک نگاه', radarFuture: 'رادار آینده', factoryReports: 'ماهنامه کارخانه', media: 'ویدیوکست و پادکست',
|
||||
glance: 'در یک نگاه', radarFuture: 'رادار آینده', technogen: 'تکنوژن', factoryReports: 'ماهنامه کارخانه', media: 'ویدیوکست و پادکست',
|
||||
events: 'رویدادها', banners: 'بنرها',
|
||||
members: 'کاربران سایت', newsletter: 'خبرنامه',
|
||||
};
|
||||
|
|
@ -184,6 +184,7 @@ function wireTabs() {
|
|||
const t = btn.dataset.tab;
|
||||
if (t === 'glance') renderRadarList();
|
||||
else if (t === 'radarFuture') renderRadarFutureList();
|
||||
else if (t === 'technogen') renderTechnogenList();
|
||||
else if (t === 'factoryReports') renderFactoryReportsList();
|
||||
else if (t === 'media') renderMediaList();
|
||||
else if (t === 'events') renderEventsList();
|
||||
|
|
@ -2052,6 +2053,167 @@ function renderFactoryReportEditor(report) {
|
|||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TECHNOGEN (تکنوژن؛ برنامه توسعهگر ملی فناوری)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function renderTechnogenList() {
|
||||
root.innerHTML = `
|
||||
${topbarHtml('technogen', '+ گزارش جدید تکنوژن')}
|
||||
<main class="page">
|
||||
<h2>تکنوژن؛ برنامه توسعهگر ملی فناوری</h2>
|
||||
<p class="muted" style="margin:-4px 0 12px">مدیریت گزارشها، مطالعات تطبیقی و یادداشتهای تحلیلی تکنوژن همراه با فایلهای PDF.</p>
|
||||
<div id="list" class="list"><p class="muted">در حال بارگذاری…</p></div>
|
||||
</main>`;
|
||||
wireTabs();
|
||||
$('#newBtn').addEventListener('click', () => renderTechnogenEditor(null));
|
||||
try {
|
||||
const items = await api('/api/technogen?limit=100');
|
||||
if (!items.length) { $('#list').innerHTML = `<p class="muted">هنوز گزارشی ثبت نشده است.</p>`; return; }
|
||||
$('#list').innerHTML = items.map(r => `
|
||||
<article class="row" data-id="${escapeAttr(r.id)}">
|
||||
<div class="row-cover" style="background-image:url('${escapeAttr(r.coverImage||'')}');background-size:contain;background-repeat:no-repeat;background-position:center;background-color:#032340;border-radius:6px;width:70px;height:90px;flex-shrink:0;"></div>
|
||||
<div class="row-main" style="margin-inline-start:12px">
|
||||
<div class="row-meta">
|
||||
${r.isFeatured ? '<span class="pill" style="background:#CD9E53;color:#032340;font-weight:bold">گزارش شاخص</span>' : ''}
|
||||
${r.code ? `<span class="pill" style="direction:ltr">${escapeHtml(r.code)}</span>` : ''}
|
||||
${r.tagFa ? `<span class="pill">${escapeHtml(r.tagFa)}</span>` : ''}
|
||||
${r.pagesFa ? `<span class="muted">${escapeHtml(r.pagesFa)}</span>` : ''}
|
||||
${r.dateFa ? `<span class="muted">${escapeHtml(r.dateFa)}</span>` : ''}
|
||||
</div>
|
||||
<h3 style="margin:4px 0 2px">${escapeHtml(r.titleFa)}</h3>
|
||||
${r.subtitleFa ? `<p class="muted clamp-2" style="font-size:13px">${escapeHtml(r.subtitleFa)}</p>` : ''}
|
||||
${r.fileUrl ? `<div style="margin-top:4px"><a href="${escapeAttr(r.fileUrl)}" target="_blank" rel="noopener noreferrer" style="font-size:12px;color:#00AEEF;text-decoration:none">📄 دانلود / مشاهده فایل PDF</a></div>` : ''}
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<button data-action="edit">ویرایش</button>
|
||||
<button data-action="delete" class="danger">حذف</button>
|
||||
</div>
|
||||
</article>`).join('');
|
||||
$('#list').addEventListener('click', async ev => {
|
||||
const btn = ev.target.closest('button[data-action]'); if (!btn) return;
|
||||
const id = btn.closest('.row').dataset.id;
|
||||
if (btn.dataset.action === 'edit') renderTechnogenEditor(await api(`/api/technogen/${id}`));
|
||||
else if (btn.dataset.action === 'delete') {
|
||||
if (!confirm('این گزارش تکنوژن حذف شود؟')) return;
|
||||
await api(`/api/technogen/${id}`, { method: 'DELETE' });
|
||||
renderTechnogenList();
|
||||
}
|
||||
});
|
||||
} catch (err) { $('#list').innerHTML = `<p class="error">خطا: ${escapeHtml(err.message)}</p>`; }
|
||||
}
|
||||
|
||||
function renderTechnogenEditor(report) {
|
||||
const r = report || {
|
||||
id: '', code: 'TGN-AN-0' + (Math.floor(Math.random() * 90) + 10),
|
||||
tagFa: 'یادداشت تحلیلی', tagEn: 'Analytical Note',
|
||||
titleFa: '', titleEn: '',
|
||||
subtitleFa: '', subtitleEn: '',
|
||||
descriptionFa: '', descriptionEn: '',
|
||||
dateFa: 'مرداد ۱۴۰۵', dateEn: 'August 2026',
|
||||
pagesFa: '۴ صفحه', pagesEn: '4 Pages',
|
||||
isFeatured: false, coverImage: '', fileUrl: '', sortOrder: 0
|
||||
};
|
||||
const isEdit = !!report;
|
||||
root.innerHTML = `
|
||||
<header class="topbar">
|
||||
<div class="brand">${isEdit ? 'ویرایش گزارش تکنوژن' : 'گزارش جدید تکنوژن'}</div>
|
||||
<div class="actions"><button id="backBtn" class="ghost">← بازگشت</button></div>
|
||||
</header>
|
||||
<main class="page editor">
|
||||
<form id="editorForm" class="grid">
|
||||
<label>کد سند (مثلاً TGN-CS-01 یا TGN-AN-01) <input name="code" value="${escapeAttr(r.code||'')}" required style="direction:ltr" /></label>
|
||||
<label class="checkbox" style="align-self:flex-end;padding-bottom:10px">
|
||||
<input type="checkbox" name="isFeatured" ${r.isFeatured ? 'checked' : ''} />
|
||||
<span>⭐ گزارش شاخص و ویژه (مطالعه تطبیقی اصلی)</span>
|
||||
</label>
|
||||
|
||||
<label>عنوان فارسی <input name="titleFa" value="${escapeAttr(r.titleFa||'')}" required placeholder="عنوان گزارش تکنوژن…" /></label>
|
||||
<label>عنوان انگلیسی <input name="titleEn" value="${escapeAttr(r.titleEn||'')}" placeholder="English Title…" style="direction:ltr" /></label>
|
||||
|
||||
<label>برچسب فارسی <input name="tagFa" value="${escapeAttr(r.tagFa||'')}" placeholder="مطالعه تطبیقی یا یادداشت تحلیلی" /></label>
|
||||
<label>برچسب انگلیسی <input name="tagEn" value="${escapeAttr(r.tagEn||'')}" placeholder="Comparative Study / Analytical Note" style="direction:ltr" /></label>
|
||||
|
||||
<label>تاریخ فارسی <input name="dateFa" value="${escapeAttr(r.dateFa||'')}" placeholder="مرداد ۱۴۰۵" /></label>
|
||||
<label>تاریخ انگلیسی <input name="dateEn" value="${escapeAttr(r.dateEn||'')}" placeholder="August 2026" style="direction:ltr" /></label>
|
||||
|
||||
<label>تعداد صفحات فارسی <input name="pagesFa" value="${escapeAttr(r.pagesFa||'')}" placeholder="۳۰ صفحه" /></label>
|
||||
<label>تعداد صفحات انگلیسی <input name="pagesEn" value="${escapeAttr(r.pagesEn||'')}" placeholder="30 Pages" style="direction:ltr" /></label>
|
||||
|
||||
<label>ترتیب نمایش <input name="sortOrder" type="number" value="${r.sortOrder||0}" /></label>
|
||||
<div></div>
|
||||
|
||||
<div class="col span-2">
|
||||
<label style="display:block;margin-bottom:6px">تصویر کاور گزارش</label>
|
||||
<input type="file" id="technogenCoverFile" accept="image/*" style="display:block;margin-bottom:8px" />
|
||||
<input type="text" name="coverImage" value="${escapeAttr(r.coverImage||'')}" placeholder="آدرس تصویر (یا از دکمه بالا آپلود کنید)" style="width:100%" />
|
||||
<img id="technogenCoverPreview" alt="" src="${escapeAttr(r.coverImage||'')}" style="${r.coverImage?'':'display:none'};max-width:220px;max-height:160px;border-radius:6px;margin-top:8px;border:1px solid #ccc;background:#032340;padding:2px" />
|
||||
</div>
|
||||
|
||||
<div class="col span-2">
|
||||
<label style="display:block;margin-bottom:6px">فایل گزارش (PDF) — آپلود مستقیم به استوریج</label>
|
||||
<input type="file" id="technogenPdfFile" accept="application/pdf" style="display:block;margin-bottom:8px" />
|
||||
<input type="text" name="fileUrl" value="${escapeAttr(r.fileUrl||'')}" placeholder="آدرس فایل PDF (یا از دکمه بالا آپلود کنید)" style="width:100%;direction:ltr" />
|
||||
<span id="technogenPdfStatus" class="muted" style="display:block;font-size:12px;margin-top:6px"></span>
|
||||
</div>
|
||||
|
||||
<div class="col span-2"><label>زیرعنوان / پیام محوری (فارسی) <textarea name="subtitleFa" rows="3" placeholder="توضیح یا شعار محوری روی جلد…">${escapeHtml(r.subtitleFa||'')}</textarea></label></div>
|
||||
<div class="col span-2"><label>زیرعنوان / پیام محوری (انگلیسی) <textarea name="subtitleEn" rows="3" style="direction:ltr">${escapeHtml(r.subtitleEn||'')}</textarea></label></div>
|
||||
|
||||
<div class="col span-2"><label>شرح تفصیلی یا نکات کلیدی (فارسی) <textarea name="descriptionFa" rows="4">${escapeHtml(r.descriptionFa||'')}</textarea></label></div>
|
||||
<div class="col span-2"><label>شرح تفصیلی یا نکات کلیدی (انگلیسی) <textarea name="descriptionEn" rows="4" style="direction:ltr">${escapeHtml(r.descriptionEn||'')}</textarea></label></div>
|
||||
|
||||
<div class="col span-2 row-end"><button type="submit" class="primary">${isEdit ? 'ذخیره تغییرات' : 'ایجاد گزارش'}</button></div>
|
||||
</form>
|
||||
</main>`;
|
||||
$('#backBtn').addEventListener('click', () => renderTechnogenList());
|
||||
|
||||
document.getElementById('technogenCoverFile').addEventListener('change', async e => {
|
||||
const file = e.target.files?.[0]; if (!file) return;
|
||||
const fd = new FormData(); fd.append('file', file);
|
||||
try {
|
||||
const { url } = await api('/api/uploads', { method:'POST', body:fd, isForm:true });
|
||||
$('input[name="coverImage"]').value = url;
|
||||
const prev = document.getElementById('technogenCoverPreview');
|
||||
if (prev) { prev.src = url; prev.style.display = ''; }
|
||||
} catch (err) { alert('آپلود تصویر ناموفق: ' + err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('technogenPdfFile').addEventListener('change', async e => {
|
||||
const file = e.target.files?.[0]; if (!file) return;
|
||||
const status = document.getElementById('technogenPdfStatus');
|
||||
if (status) status.textContent = 'در حال آپلود فایل PDF…';
|
||||
const fd = new FormData(); fd.append('file', file);
|
||||
try {
|
||||
const { url } = await api('/api/uploads/raw', { method:'POST', body:fd, isForm:true });
|
||||
$('input[name="fileUrl"]').value = url;
|
||||
if (status) status.textContent = '✓ فایل PDF با موفقیت آپلود شد: ' + url;
|
||||
} catch (err) { if (status) status.textContent = ''; alert('آپلود PDF ناموفق: ' + err.message); }
|
||||
});
|
||||
|
||||
$('#editorForm').addEventListener('submit', async ev => {
|
||||
ev.preventDefault();
|
||||
const fd = new FormData(ev.target);
|
||||
const payload = {
|
||||
code: fd.get('code') || null,
|
||||
isFeatured: fd.get('isFeatured') === 'on',
|
||||
titleFa: fd.get('titleFa'), titleEn: fd.get('titleEn') || null,
|
||||
tagFa: fd.get('tagFa') || null, tagEn: fd.get('tagEn') || null,
|
||||
dateFa: fd.get('dateFa') || null, dateEn: fd.get('dateEn') || null,
|
||||
pagesFa: fd.get('pagesFa') || null, pagesEn: fd.get('pagesEn') || null,
|
||||
coverImage: fd.get('coverImage') || null,
|
||||
fileUrl: fd.get('fileUrl') || null,
|
||||
subtitleFa: fd.get('subtitleFa') || null, subtitleEn: fd.get('subtitleEn') || null,
|
||||
descriptionFa: fd.get('descriptionFa') || null, descriptionEn: fd.get('descriptionEn') || null,
|
||||
sortOrder: Number(fd.get('sortOrder') || 0),
|
||||
};
|
||||
try {
|
||||
if (isEdit) await api(`/api/technogen/${report.id}`, { method:'PUT', body:payload });
|
||||
else await api('/api/technogen', { method:'POST', body:payload });
|
||||
renderTechnogenList();
|
||||
} catch (err) { alert('ذخیره ناموفق: ' + err.message); }
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// INTEGRATIONS (منابع داده)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
|
@ -16,7 +16,7 @@ import fs from 'node:fs';
|
|||
import os from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { randomInt, createHmac, randomUUID } from 'node:crypto';
|
||||
import { db, rowToArticle, rowToRiskSignal, rowToPrice, rowToEvent, rowToTeamMember, rowToPlan, rowToBanner, rowToRadarItem, rowToRadarPage, rowToFactoryReport, rowToIntegration, rowToInstituteStat, rowToMarketPrice, rowToMarketChartPoint, rowToVisionItem, rowToAdvisoryMember } from './db.js';
|
||||
import { db, rowToArticle, rowToRiskSignal, rowToPrice, rowToEvent, rowToTeamMember, rowToPlan, rowToBanner, rowToRadarItem, rowToRadarPage, rowToFactoryReport, rowToTechnogenReport, rowToIntegration, rowToInstituteStat, rowToMarketPrice, rowToMarketChartPoint, rowToVisionItem, rowToAdvisoryMember } from './db.js';
|
||||
import { getJob, listJobs, runGenerator } from './content-generator.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -1134,6 +1134,81 @@ app.delete('/api/factory-reports/:id', authRequired, async (req, res) => {
|
|||
res.status(204).end();
|
||||
});
|
||||
|
||||
// ---------- technogen reports ----------
|
||||
const TECHNOGEN_FIELDS = `id, code, tag_fa, tag_en, title_fa, title_en, subtitle_fa, subtitle_en,
|
||||
description_fa, description_en, date_fa, date_en, pages_fa, pages_en,
|
||||
is_featured, cover_image, file_url, sort_order, created_at, updated_at`;
|
||||
|
||||
app.get('/api/technogen', async (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
||||
const rows = await db.prepare(`SELECT ${TECHNOGEN_FIELDS} FROM technogen_reports ORDER BY is_featured DESC, sort_order ASC, created_at DESC LIMIT ?`).all(limit);
|
||||
res.json(rows.map(rowToTechnogenReport));
|
||||
});
|
||||
|
||||
app.get('/api/technogen/:id', async (req, res) => {
|
||||
const row = await db.prepare(`SELECT ${TECHNOGEN_FIELDS} FROM technogen_reports WHERE id = ?`).get(req.params.id);
|
||||
if (!row) return res.status(404).json({ error: 'not_found' });
|
||||
res.json(rowToTechnogenReport(row));
|
||||
});
|
||||
|
||||
function normalizeTechnogenBody(b) {
|
||||
return {
|
||||
code: b.code ? String(b.code).trim() : null,
|
||||
tag_fa: b.tagFa || null,
|
||||
tag_en: b.tagEn || null,
|
||||
title_fa: String(b.titleFa || '').trim(),
|
||||
title_en: b.titleEn || null,
|
||||
subtitle_fa: b.subtitleFa || null,
|
||||
subtitle_en: b.subtitleEn || null,
|
||||
description_fa: b.descriptionFa || null,
|
||||
description_en: b.descriptionEn || null,
|
||||
date_fa: b.dateFa || null,
|
||||
date_en: b.dateEn || null,
|
||||
pages_fa: b.pagesFa || null,
|
||||
pages_en: b.pagesEn || null,
|
||||
is_featured: b.isFeatured ? 1 : 0,
|
||||
cover_image: b.coverImage || null,
|
||||
file_url: b.fileUrl || null,
|
||||
sort_order: Number(b.sortOrder) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
app.post('/api/technogen', authRequired, async (req, res) => {
|
||||
const r = normalizeTechnogenBody(req.body || {});
|
||||
if (!r.title_fa) return res.status(400).json({ error: 'title_fa_required' });
|
||||
const id = req.body.id ? String(req.body.id).trim().toLowerCase() : nanoid(14);
|
||||
await db.prepare(`INSERT INTO technogen_reports (
|
||||
id, code, tag_fa, tag_en, title_fa, title_en, subtitle_fa, subtitle_en,
|
||||
description_fa, description_en, date_fa, date_en, pages_fa, pages_en,
|
||||
is_featured, cover_image, file_url, sort_order
|
||||
) VALUES (
|
||||
@id, @code, @tag_fa, @tag_en, @title_fa, @title_en, @subtitle_fa, @subtitle_en,
|
||||
@description_fa, @description_en, @date_fa, @date_en, @pages_fa, @pages_en,
|
||||
@is_featured, @cover_image, @file_url, @sort_order
|
||||
)`).run({ id, ...r });
|
||||
res.status(201).json(rowToTechnogenReport(await db.prepare(`SELECT ${TECHNOGEN_FIELDS} FROM technogen_reports WHERE id = ?`).get(id)));
|
||||
});
|
||||
|
||||
app.put('/api/technogen/:id', authRequired, async (req, res) => {
|
||||
if (!await db.prepare('SELECT id FROM technogen_reports WHERE id = ?').get(req.params.id)) return res.status(404).json({ error: 'not_found' });
|
||||
const r = normalizeTechnogenBody(req.body || {});
|
||||
if (!r.title_fa) return res.status(400).json({ error: 'title_fa_required' });
|
||||
await db.prepare(`UPDATE technogen_reports SET
|
||||
code=@code, tag_fa=@tag_fa, tag_en=@tag_en, title_fa=@title_fa, title_en=@title_en,
|
||||
subtitle_fa=@subtitle_fa, subtitle_en=@subtitle_en, description_fa=@description_fa, description_en=@description_en,
|
||||
date_fa=@date_fa, date_en=@date_en, pages_fa=@pages_fa, pages_en=@pages_en,
|
||||
is_featured=@is_featured, cover_image=@cover_image, file_url=@file_url,
|
||||
sort_order=@sort_order, updated_at=now() WHERE id=@id
|
||||
`).run({ id: req.params.id, ...r });
|
||||
res.json(rowToTechnogenReport(await db.prepare(`SELECT ${TECHNOGEN_FIELDS} FROM technogen_reports WHERE id = ?`).get(req.params.id)));
|
||||
});
|
||||
|
||||
app.delete('/api/technogen/:id', authRequired, async (req, res) => {
|
||||
const info = await db.prepare('DELETE FROM technogen_reports WHERE id = ?').run(req.params.id);
|
||||
if (info.changes === 0) return res.status(404).json({ error: 'not_found' });
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
// ---------- integrations ----------
|
||||
const INTEGRATION_FIELDS = `id, name, icon, logo, accent, grid_col, grid_row, sort_order, created_at, updated_at`;
|
||||
const INTEGRATION_ICONS = ['exchange','globe','chart','activity','database','newspaper','building','trending','coins'];
|
||||
|
|
|
|||