Initial commit: TopSysOps site + admin panel
Multi-page public site (Next.js), Node/Express backend with Postgres, and Vite admin/account app, restructured from a single-page layout into dedicated service, promotions, cases, about, FAQ, and blog pages for SEO. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
commit
130c5382cd
108 files changed
+7146
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle2 } from 'lucide-react';
|
||||
import Header from '../../../components/Header.jsx';
|
||||
import Footer from '../../../components/Footer.jsx';
|
||||
import Reveal from '../../../components/Reveal.jsx';
|
||||
import { ServicePriceList } from '../../../components/sections/Services.jsx';
|
||||
import { getIcon } from '../../../lib/icons.js';
|
||||
import { getServiceLandingCopy } from '../../../lib/serviceLandingCopy.js';
|
||||
import { serverApi } from '../../../lib/serverApi.js';
|
||||
|
||||
async function findCategory(slug) {
|
||||
const services = await serverApi.getServices();
|
||||
return services.find((c) => c.slug === slug) || null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const category = await findCategory(params.slug);
|
||||
if (!category) return { title: 'Услуга не найдена' };
|
||||
const copy = getServiceLandingCopy(category);
|
||||
return { title: copy.metaTitle, description: copy.metaDescription };
|
||||
}
|
||||
|
||||
export default async function ServiceLandingPage({ params }) {
|
||||
const category = await findCategory(params.slug);
|
||||
if (!category) notFound();
|
||||
|
||||
const copy = getServiceLandingCopy(category);
|
||||
const Icon = getIcon(category.icon);
|
||||
|
||||
const serviceSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: copy.h1,
|
||||
description: copy.metaDescription,
|
||||
provider: { '@type': 'Organization', name: 'TopSysOps' },
|
||||
areaServed: 'RU',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(serviceSchema) }} />
|
||||
|
||||
<Header />
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pt-16 pb-4">
|
||||
<Link href="/uslugi" className="text-sm text-emerald-500 font-medium inline-flex items-center gap-1 mb-8">
|
||||
<ArrowLeft className="h-3.5 w-3.5" /> Все услуги
|
||||
</Link>
|
||||
<Reveal>
|
||||
<Icon className="h-8 w-8 text-emerald-500 mb-4" />
|
||||
<h1 className="text-2xl sm:text-3xl font-bold mb-4 font-display">{copy.h1}</h1>
|
||||
<p className="text-base text-slate-600 dark:text-slate-400 max-w-2xl mb-6">{copy.intro}</p>
|
||||
</Reveal>
|
||||
|
||||
{copy.bullets.length > 0 && (
|
||||
<Reveal delay={80}>
|
||||
<ul className="space-y-2 mb-10">
|
||||
{copy.bullets.map((b) => (
|
||||
<li key={b} className="flex items-start gap-2 text-sm text-slate-700 dark:text-slate-300">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-500 mt-0.5 flex-shrink-0" />
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pb-20">
|
||||
<Reveal delay={120}>
|
||||
<div className="rounded-lg border p-5 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
||||
<h2 className="font-bold mb-1 font-display text-[15px]">{category.title}: цены</h2>
|
||||
<p className="text-xs mb-4 text-slate-500 dark:text-slate-400">{category.description}</p>
|
||||
<ServicePriceList category={category} />
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={160} className="mt-8">
|
||||
<Link
|
||||
href="/kontakty"
|
||||
className="inline-flex items-center gap-2 px-5 py-3 rounded-md bg-emerald-500 text-white font-medium text-sm hover:bg-emerald-600 transition-colors"
|
||||
>
|
||||
Обсудить задачу <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Reveal>
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Services from '../../components/sections/Services.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Услуги IT-аутсорсинга и техподдержки — прайс-лист',
|
||||
description:
|
||||
'IT-аутсорсинг, техподдержка, кибербезопасность, администрирование серверов и баз данных, сетевая инфраструктура, облачные решения, ИИ и автоматизация. Актуальные цены.',
|
||||
};
|
||||
|
||||
export default async function ServicesPage() {
|
||||
const services = await serverApi.getServices();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Services categories={services} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user