feat: fetch categories in menu
This commit is contained in:
@@ -4,17 +4,32 @@ import '../styles/globals.css'
|
|||||||
import { getCollection, type CollectionEntry } from 'astro:content'
|
import { getCollection, type CollectionEntry } from 'astro:content'
|
||||||
import { Header } from '../sections/Header'
|
import { Header } from '../sections/Header'
|
||||||
import { Footer } from '../sections/Footer'
|
import { Footer } from '../sections/Footer'
|
||||||
|
import { Enum_Category_Group } from '../graphql/graphql'
|
||||||
|
import { categoriesListQuery } from '../graphql-documents/categories.gql.generated'
|
||||||
import { CategoryView } from '../views/CategoryView'
|
import { CategoryView } from '../views/CategoryView'
|
||||||
|
import type { CategoriesListQuery } from '../graphql/graphql'
|
||||||
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
||||||
import { siteConfigQuery } from '../graphql-documents/site-config.gql.generated'
|
import { siteConfigQuery } from '../graphql-documents/site-config.gql.generated'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const categories = await getCollection('categories')
|
const categories = await getCollection('categories')
|
||||||
const siteResult = await siteConfigQuery.execute()
|
const [siteResult, solutionCategoriesResult, serviceCategoriesResult] = await Promise.all([
|
||||||
|
siteConfigQuery.execute(),
|
||||||
|
categoriesListQuery.execute({ group: Enum_Category_Group.Solution }),
|
||||||
|
categoriesListQuery.execute({ group: Enum_Category_Group.Service }),
|
||||||
|
])
|
||||||
if (siteResult.errors?.length) {
|
if (siteResult.errors?.length) {
|
||||||
throw new Error(siteResult.errors.map(e => e.message).join(', '))
|
throw new Error(siteResult.errors.map(e => e.message).join(', '))
|
||||||
}
|
}
|
||||||
|
if (solutionCategoriesResult.errors?.length) {
|
||||||
|
throw new Error(solutionCategoriesResult.errors.map(e => e.message).join(', '))
|
||||||
|
}
|
||||||
|
if (serviceCategoriesResult.errors?.length) {
|
||||||
|
throw new Error(serviceCategoriesResult.errors.map(e => e.message).join(', '))
|
||||||
|
}
|
||||||
const contacts = siteResult.data?.siteConfig?.contacts?.filter(c => c != null) ?? []
|
const contacts = siteResult.data?.siteConfig?.contacts?.filter(c => c != null) ?? []
|
||||||
|
const solutionCategories = solutionCategoriesResult.data?.categories?.filter(c => c != null) ?? []
|
||||||
|
const serviceCategories = serviceCategoriesResult.data?.categories?.filter(c => c != null) ?? []
|
||||||
|
|
||||||
return categories.map((entry: CollectionEntry<'categories'>) => ({
|
return categories.map((entry: CollectionEntry<'categories'>) => ({
|
||||||
params: { slug: entry.id },
|
params: { slug: entry.id },
|
||||||
@@ -23,6 +38,8 @@ export async function getStaticPaths() {
|
|||||||
description: entry.data.description,
|
description: entry.data.description,
|
||||||
vendors: entry.data.vendors,
|
vendors: entry.data.vendors,
|
||||||
contacts,
|
contacts,
|
||||||
|
solutionCategories,
|
||||||
|
serviceCategories,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -36,15 +53,22 @@ interface Props {
|
|||||||
products?: string[]
|
products?: string[]
|
||||||
}[]
|
}[]
|
||||||
contacts: SiteConfigContact[]
|
contacts: SiteConfigContact[]
|
||||||
|
solutionCategories: NonNullable<CategoriesListQuery['categories'][number]>[]
|
||||||
|
serviceCategories: NonNullable<CategoriesListQuery['categories'][number]>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, vendors, contacts } = Astro.props as Props
|
const { title, description, vendors, contacts, solutionCategories, serviceCategories } = Astro.props as Props
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
||||||
<Header client:load contacts={contacts} />
|
<Header
|
||||||
|
client:load
|
||||||
|
contacts={contacts}
|
||||||
|
solutionCategories={solutionCategories}
|
||||||
|
serviceCategories={serviceCategories}
|
||||||
|
/>
|
||||||
<CategoryView client:load title={title} description={description} vendors={vendors} />
|
<CategoryView client:load title={title} description={description} vendors={vendors} />
|
||||||
<Footer client:load contacts={contacts} />
|
<Footer client:load contacts={contacts} solutionCategories={solutionCategories} />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import '../styles/globals.css'
|
|||||||
import { Header } from '../sections/Header'
|
import { Header } from '../sections/Header'
|
||||||
import { Footer } from '../sections/Footer'
|
import { Footer } from '../sections/Footer'
|
||||||
import { HomePage } from '../views/HomePage'
|
import { HomePage } from '../views/HomePage'
|
||||||
|
import { Enum_Category_Group } from '../graphql/graphql'
|
||||||
|
import { categoriesListQuery } from '../graphql-documents/categories.gql.generated'
|
||||||
import { homePageQuery } from '../graphql-documents/home-page.gql.generated'
|
import { homePageQuery } from '../graphql-documents/home-page.gql.generated'
|
||||||
import { siteConfigQuery } from '../graphql-documents/site-config.gql.generated'
|
import { siteConfigQuery } from '../graphql-documents/site-config.gql.generated'
|
||||||
|
|
||||||
const [homeResult, siteResult] = await Promise.all([
|
const [homeResult, siteResult, solutionCategoriesResult, serviceCategoriesResult] = await Promise.all([
|
||||||
homePageQuery.execute(),
|
homePageQuery.execute(),
|
||||||
siteConfigQuery.execute(),
|
siteConfigQuery.execute(),
|
||||||
|
categoriesListQuery.execute({ group: Enum_Category_Group.Solution }),
|
||||||
|
categoriesListQuery.execute({ group: Enum_Category_Group.Service }),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (homeResult.errors?.length) {
|
if (homeResult.errors?.length) {
|
||||||
@@ -18,15 +22,28 @@ if (homeResult.errors?.length) {
|
|||||||
if (siteResult.errors?.length) {
|
if (siteResult.errors?.length) {
|
||||||
throw new Error(siteResult.errors.map(e => e.message).join(', '))
|
throw new Error(siteResult.errors.map(e => e.message).join(', '))
|
||||||
}
|
}
|
||||||
|
if (solutionCategoriesResult.errors?.length) {
|
||||||
|
throw new Error(solutionCategoriesResult.errors.map(e => e.message).join(', '))
|
||||||
|
}
|
||||||
|
if (serviceCategoriesResult.errors?.length) {
|
||||||
|
throw new Error(serviceCategoriesResult.errors.map(e => e.message).join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
const homePage = homeResult.data?.homePage ?? null
|
const homePage = homeResult.data?.homePage ?? null
|
||||||
const contacts = siteResult.data?.siteConfig?.contacts?.filter(c => c != null) ?? []
|
const contacts = siteResult.data?.siteConfig?.contacts?.filter(c => c != null) ?? []
|
||||||
|
const solutionCategories = solutionCategoriesResult.data?.categories?.filter(c => c != null) ?? []
|
||||||
|
const serviceCategories = serviceCategoriesResult.data?.categories?.filter(c => c != null) ?? []
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
||||||
<Header client:load contacts={contacts} />
|
<Header
|
||||||
|
client:load
|
||||||
|
contacts={contacts}
|
||||||
|
solutionCategories={solutionCategories}
|
||||||
|
serviceCategories={serviceCategories}
|
||||||
|
/>
|
||||||
<HomePage client:load homePage={homePage} contacts={contacts} />
|
<HomePage client:load homePage={homePage} contacts={contacts} />
|
||||||
<Footer client:load contacts={contacts} />
|
<Footer client:load contacts={contacts} solutionCategories={solutionCategories} />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Send, Video } from 'lucide-react'
|
import { Send, Video } from 'lucide-react'
|
||||||
|
|
||||||
import { SiteContactItems } from '../components/SiteContactItems'
|
import { SiteContactItems } from '../components/SiteContactItems'
|
||||||
|
import type { CategoriesListQuery } from '../graphql/graphql'
|
||||||
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
||||||
|
|
||||||
export interface FooterProps {
|
export interface FooterProps {
|
||||||
contacts: readonly SiteConfigContact[]
|
contacts: readonly SiteConfigContact[]
|
||||||
|
solutionCategories: ReadonlyArray<NonNullable<CategoriesListQuery['categories'][number]>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Footer({ contacts }: FooterProps) {
|
export function Footer({ contacts, solutionCategories }: FooterProps) {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gray-900 text-gray-400 pt-20 pb-10 border-t border-gray-800">
|
<footer className="bg-gray-900 text-gray-400 pt-20 pb-10 border-t border-gray-800">
|
||||||
<div className="max-w-400 mx-auto px-6 xl:px-12 2xl:px-16">
|
<div className="max-w-400 mx-auto px-6 xl:px-12 2xl:px-16">
|
||||||
@@ -59,26 +61,13 @@ export function Footer({ contacts }: FooterProps) {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="text-white mb-6">Решения</h4>
|
<h4 className="text-white mb-6">Решения</h4>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
<li>
|
{solutionCategories.map(category => (
|
||||||
<a href="#" className="hover:text-blue-400 transition-colors">
|
<li key={category.documentId}>
|
||||||
Облачные решения
|
<a href={`/${category.slug}`} className="hover:text-blue-400 transition-colors">
|
||||||
</a>
|
{category.title ?? category.slug}
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" className="hover:text-blue-400 transition-colors">
|
|
||||||
Кибербезопасность
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" className="hover:text-blue-400 transition-colors">
|
|
||||||
Цифровая трансформация
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" className="hover:text-blue-400 transition-colors">
|
|
||||||
Управление данными
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,26 @@
|
|||||||
import { Menu, X, ChevronDown } from 'lucide-react'
|
import { Menu, X, ChevronDown } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ContactModal } from '../components/ContactModal'
|
import { ContactModal } from '../components/ContactModal'
|
||||||
|
import type { CategoriesListQuery } from '../graphql/graphql'
|
||||||
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
||||||
import { navigateHomeSectionAnchor } from '../lib/scrollToHash'
|
import { navigateHomeSectionAnchor } from '../lib/scrollToHash'
|
||||||
|
|
||||||
|
type CategoryNavItem = NonNullable<CategoriesListQuery['categories'][number]>
|
||||||
|
|
||||||
export interface HeaderProps {
|
export interface HeaderProps {
|
||||||
contacts: readonly SiteConfigContact[]
|
contacts: readonly SiteConfigContact[]
|
||||||
|
solutionCategories: readonly CategoryNavItem[]
|
||||||
|
serviceCategories: readonly CategoryNavItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ contacts }: HeaderProps) {
|
export function Header({ contacts, solutionCategories, serviceCategories }: HeaderProps) {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
const [isSolutionsOpen, setIsSolutionsOpen] = useState(false)
|
const [isSolutionsOpen, setIsSolutionsOpen] = useState(false)
|
||||||
const [isServicesOpen, setIsServicesOpen] = useState(false)
|
const [isServicesOpen, setIsServicesOpen] = useState(false)
|
||||||
const [isContactModalOpen, setIsContactModalOpen] = useState(false)
|
const [isContactModalOpen, setIsContactModalOpen] = useState(false)
|
||||||
|
|
||||||
const solutions = [
|
const solutions = [...solutionCategories].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
{ name: 'Облако', path: 'cloud' },
|
const services = [...serviceCategories].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
{ name: 'Российское ПО', path: 'russian-software' },
|
|
||||||
{ name: 'Кибербезопасность', path: 'cybersecurity' },
|
|
||||||
{ name: 'Решения на базе ИИ', path: 'ai-solutions' },
|
|
||||||
{ name: 'Российские ноутбуки, ПК и серверы', path: 'russian-hardware' },
|
|
||||||
{ name: 'Аппаратные решения', path: 'hardware-solutions' },
|
|
||||||
{ name: 'САПР и ГИС', path: 'cad-gis' },
|
|
||||||
{ name: 'Индустриальные решения', path: 'industrial-solutions' },
|
|
||||||
{ name: 'Для госсектора', path: 'government' },
|
|
||||||
{ name: 'Решения для бизнеса', path: 'business' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const services = [
|
|
||||||
{ name: 'Консалтинг и проектирование', path: 'consulting' },
|
|
||||||
{ name: 'Поставка и внедрение решений', path: 'implementation' },
|
|
||||||
{ name: 'Управляемые сервисы', path: 'managed-services' },
|
|
||||||
{ name: 'Интеграция и разработка', path: 'integration' },
|
|
||||||
{ name: 'Специализированные услуги по отраслям', path: 'specialized-services' },
|
|
||||||
{ name: 'Обслуживание и сопровождение', path: 'support' },
|
|
||||||
{ name: 'Услуги по подписке', path: 'subscription' },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
|
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
|
||||||
@@ -47,6 +32,7 @@ export function Header({ contacts }: HeaderProps) {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className="hidden lg:flex items-center gap-8">
|
<div className="hidden lg:flex items-center gap-8">
|
||||||
|
{solutions.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
onMouseEnter={() => setIsSolutionsOpen(true)}
|
onMouseEnter={() => setIsSolutionsOpen(true)}
|
||||||
@@ -62,17 +48,19 @@ export function Header({ contacts }: HeaderProps) {
|
|||||||
<div className="w-80 bg-white rounded-xl shadow-xl border border-gray-100 py-3 animate-in fade-in slide-in-from-top-2 duration-200">
|
<div className="w-80 bg-white rounded-xl shadow-xl border border-gray-100 py-3 animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
{solutions.map(solution => (
|
{solutions.map(solution => (
|
||||||
<a
|
<a
|
||||||
key={solution.path}
|
key={solution.documentId}
|
||||||
href={`/${solution.path}`}
|
href={`/${solution.slug}`}
|
||||||
className="block w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
|
className="block w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
|
||||||
>
|
>
|
||||||
{solution.name}
|
{solution.title ?? solution.slug}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{services.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
onMouseEnter={() => setIsServicesOpen(true)}
|
onMouseEnter={() => setIsServicesOpen(true)}
|
||||||
@@ -88,17 +76,18 @@ export function Header({ contacts }: HeaderProps) {
|
|||||||
<div className="w-80 bg-white rounded-xl shadow-xl border border-gray-100 py-3 animate-in fade-in slide-in-from-top-2 duration-200">
|
<div className="w-80 bg-white rounded-xl shadow-xl border border-gray-100 py-3 animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
{services.map(service => (
|
{services.map(service => (
|
||||||
<a
|
<a
|
||||||
key={service.path}
|
key={service.documentId}
|
||||||
href={`/${service.path}`}
|
href={`/${service.slug}`}
|
||||||
className="block w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
|
className="block w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
|
||||||
>
|
>
|
||||||
{service.name}
|
{service.title ?? service.slug}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href="/#cases"
|
href="/#cases"
|
||||||
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
||||||
@@ -140,6 +129,7 @@ export function Header({ contacts }: HeaderProps) {
|
|||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<div className="lg:hidden pt-4 pb-2">
|
<div className="lg:hidden pt-4 pb-2">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
{solutions.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -153,20 +143,22 @@ export function Header({ contacts }: HeaderProps) {
|
|||||||
<div className="pl-4 mt-2 space-y-2">
|
<div className="pl-4 mt-2 space-y-2">
|
||||||
{solutions.map(solution => (
|
{solutions.map(solution => (
|
||||||
<a
|
<a
|
||||||
key={solution.path}
|
key={solution.documentId}
|
||||||
href={`/${solution.path}`}
|
href={`/${solution.slug}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsMenuOpen(false)
|
setIsMenuOpen(false)
|
||||||
setIsSolutionsOpen(false)
|
setIsSolutionsOpen(false)
|
||||||
}}
|
}}
|
||||||
className="block w-full text-left text-sm text-gray-600 hover:text-blue-600 py-2"
|
className="block w-full text-left text-sm text-gray-600 hover:text-blue-600 py-2"
|
||||||
>
|
>
|
||||||
{solution.name}
|
{solution.title ?? solution.slug}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{services.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -180,20 +172,21 @@ export function Header({ contacts }: HeaderProps) {
|
|||||||
<div className="pl-4 mt-2 space-y-2">
|
<div className="pl-4 mt-2 space-y-2">
|
||||||
{services.map(service => (
|
{services.map(service => (
|
||||||
<a
|
<a
|
||||||
key={service.path}
|
key={service.documentId}
|
||||||
href={`/${service.path}`}
|
href={`/${service.slug}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsMenuOpen(false)
|
setIsMenuOpen(false)
|
||||||
setIsServicesOpen(false)
|
setIsServicesOpen(false)
|
||||||
}}
|
}}
|
||||||
className="block w-full text-left text-sm text-gray-600 hover:text-blue-600 py-2"
|
className="block w-full text-left text-sm text-gray-600 hover:text-blue-600 py-2"
|
||||||
>
|
>
|
||||||
{service.name}
|
{service.title ?? service.slug}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href="/#cases"
|
href="/#cases"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
|||||||
Reference in New Issue
Block a user