chore: show menu item only if section anchor provided
This commit is contained in:
@@ -9,6 +9,7 @@ export const homePageDocument = gql`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
badge
|
||||
stats {
|
||||
id
|
||||
@@ -20,6 +21,7 @@ export const homePageDocument = gql`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
cards {
|
||||
id
|
||||
title
|
||||
@@ -39,6 +41,7 @@ export const homePageDocument = gql`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
cards {
|
||||
id
|
||||
title
|
||||
@@ -56,6 +59,7 @@ export const homePageDocument = gql`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
cards {
|
||||
id
|
||||
title
|
||||
@@ -75,6 +79,7 @@ export const homePageDocument = gql`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
partners(pagination: { pageSize: 100 }) {
|
||||
id
|
||||
value
|
||||
|
||||
@@ -1611,11 +1611,11 @@ export type HomePageQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type HomePageQuery = { __typename?: 'Query', homePage?: { __typename?: 'HomePage', sections?: Array<
|
||||
| { __typename: 'ComponentSectionsAbout', id: string, title?: string | null, description?: string | null, partners?: Array<{ __typename?: 'ComponentSharedString', id: string, value: string } | null> | null, cards?: Array<{ __typename?: 'ComponentCardsAdvantage', id: string, title?: string | null, description?: string | null, color?: { __typename?: 'ComponentSharedColor', value?: Enum_Componentsharedcolor_Value | null } | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsCases', id: string, title?: string | null, description?: string | null, cards?: Array<{ __typename?: 'ComponentCardsCase', id: string, title?: string | null, description?: string | null, pretitle?: string | null, badge?: string | null, footnote?: string | null, color?: { __typename?: 'ComponentSharedColor', value?: Enum_Componentsharedcolor_Value | null } | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsHomeHero', id: string, title?: string | null, description?: string | null, badge?: string | null, stats?: Array<{ __typename?: 'ComponentSharedAttribute', id: string, key?: string | null, value?: string | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsServices', id: string, title?: string | null, description?: string | null, cards?: Array<{ __typename?: 'ComponentCardsService', id: string, title?: string | null, description?: string | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null, color?: { __typename?: 'ComponentSharedColor', value?: Enum_Componentsharedcolor_Value | null } | null, category?: { __typename?: 'Category', slug: string } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsSolutions', id: string, title?: string | null, description?: string | null, cards?: Array<{ __typename?: 'ComponentCardsSolution', id: string, title?: string | null, description?: string | null, footnote?: string | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null, category?: { __typename?: 'Category', slug: string } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsAbout', id: string, title?: string | null, description?: string | null, anchor?: string | null, partners?: Array<{ __typename?: 'ComponentSharedString', id: string, value: string } | null> | null, cards?: Array<{ __typename?: 'ComponentCardsAdvantage', id: string, title?: string | null, description?: string | null, color?: { __typename?: 'ComponentSharedColor', value?: Enum_Componentsharedcolor_Value | null } | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsCases', id: string, title?: string | null, description?: string | null, anchor?: string | null, cards?: Array<{ __typename?: 'ComponentCardsCase', id: string, title?: string | null, description?: string | null, pretitle?: string | null, badge?: string | null, footnote?: string | null, color?: { __typename?: 'ComponentSharedColor', value?: Enum_Componentsharedcolor_Value | null } | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsHomeHero', id: string, title?: string | null, description?: string | null, anchor?: string | null, badge?: string | null, stats?: Array<{ __typename?: 'ComponentSharedAttribute', id: string, key?: string | null, value?: string | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsServices', id: string, title?: string | null, description?: string | null, anchor?: string | null, cards?: Array<{ __typename?: 'ComponentCardsService', id: string, title?: string | null, description?: string | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null, color?: { __typename?: 'ComponentSharedColor', value?: Enum_Componentsharedcolor_Value | null } | null, category?: { __typename?: 'Category', slug: string } | null } | null> | null }
|
||||
| { __typename: 'ComponentSectionsSolutions', id: string, title?: string | null, description?: string | null, anchor?: string | null, cards?: Array<{ __typename?: 'ComponentCardsSolution', id: string, title?: string | null, description?: string | null, footnote?: string | null, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_Value | null } | null, category?: { __typename?: 'Category', slug: string } | null } | null> | null }
|
||||
| { __typename: 'Error' }
|
||||
| null> | null } | null };
|
||||
|
||||
@@ -1677,6 +1677,7 @@ export const HomePageDocument = new TypedDocumentString(`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
badge
|
||||
stats {
|
||||
id
|
||||
@@ -1688,6 +1689,7 @@ export const HomePageDocument = new TypedDocumentString(`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
cards {
|
||||
id
|
||||
title
|
||||
@@ -1707,6 +1709,7 @@ export const HomePageDocument = new TypedDocumentString(`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
cards {
|
||||
id
|
||||
title
|
||||
@@ -1724,6 +1727,7 @@ export const HomePageDocument = new TypedDocumentString(`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
cards {
|
||||
id
|
||||
title
|
||||
@@ -1743,6 +1747,7 @@ export const HomePageDocument = new TypedDocumentString(`
|
||||
id
|
||||
title
|
||||
description
|
||||
anchor
|
||||
partners(pagination: {pageSize: 100}) {
|
||||
id
|
||||
value
|
||||
|
||||
46
src/lib/cms/sections/homeNav.ts
Normal file
46
src/lib/cms/sections/homeNav.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { HomePageQuery } from '../../../graphql/graphql'
|
||||
import type { Maybe } from '../../types'
|
||||
|
||||
type HomePageData = NonNullable<HomePageQuery['homePage']>
|
||||
|
||||
/**
|
||||
* Anchors configured on each home-page section, keyed by section kind. A key is
|
||||
* present only when its section exists and has a non-empty `anchor`, which is
|
||||
* what gates the matching header menu item.
|
||||
*/
|
||||
export interface HomeSectionAnchors {
|
||||
hero?: string
|
||||
services?: string
|
||||
solutions?: string
|
||||
cases?: string
|
||||
about?: string
|
||||
}
|
||||
|
||||
export function getHomeSectionAnchors(home: Maybe<HomePageData>): HomeSectionAnchors {
|
||||
const anchors: HomeSectionAnchors = {}
|
||||
|
||||
for (const section of home?.sections ?? []) {
|
||||
if (!section || section.__typename === 'Error') continue
|
||||
const anchor = section.anchor?.trim()
|
||||
if (!anchor) continue
|
||||
switch (section.__typename) {
|
||||
case 'ComponentSectionsHomeHero':
|
||||
anchors.hero = anchor
|
||||
break
|
||||
case 'ComponentSectionsServices':
|
||||
anchors.services = anchor
|
||||
break
|
||||
case 'ComponentSectionsSolutions':
|
||||
anchors.solutions = anchor
|
||||
break
|
||||
case 'ComponentSectionsCases':
|
||||
anchors.cases = anchor
|
||||
break
|
||||
case 'ComponentSectionsAbout':
|
||||
anchors.about = anchor
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return anchors
|
||||
}
|
||||
@@ -8,12 +8,22 @@ import { CategoryView } from '../views/CategoryView'
|
||||
import { siteConfigQuery } from '../graphql-documents/site-config.gql.generated'
|
||||
import { menuItemsQuery } from '../graphql-documents/menu-items.gql.generated'
|
||||
import { categoryQuery } from '../graphql-documents/category.gql.generated'
|
||||
import { homePageQuery } from '../graphql-documents/home-page.gql.generated'
|
||||
import { resolveUploadUrl } from '../lib/cms/uploads/url'
|
||||
import { getHomeSectionAnchors } from '../lib/cms/sections/homeNav'
|
||||
|
||||
const [siteResult, menuItemsResult] = await Promise.all([siteConfigQuery.execute(), menuItemsQuery.execute()])
|
||||
const [siteResult, menuItemsResult, homeResult] = await Promise.all([
|
||||
siteConfigQuery.execute(),
|
||||
menuItemsQuery.execute(),
|
||||
homePageQuery.execute(),
|
||||
])
|
||||
if (siteResult.errors?.length) {
|
||||
throw new Error(siteResult.errors.map(e => e.message).join(', '))
|
||||
}
|
||||
if (homeResult.errors?.length) {
|
||||
throw new Error(homeResult.errors.map(e => e.message).join(', '))
|
||||
}
|
||||
const sectionAnchors = getHomeSectionAnchors(homeResult.data?.homePage ?? null)
|
||||
const siteConfig = siteResult.data?.siteConfig
|
||||
const siteTitle = siteConfig?.title ?? ''
|
||||
const siteDescription = siteConfig?.description ?? ''
|
||||
@@ -62,7 +72,7 @@ const vendors = (category.vendors?.filter(v => v != null) ?? []).map(vendor => (
|
||||
description={description ?? siteDescription}
|
||||
>
|
||||
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
||||
<Header client:load siteTitle={siteTitle} contacts={contacts} solutions={solutionCategories} services={serviceCategories} />
|
||||
<Header client:load siteTitle={siteTitle} contacts={contacts} solutions={solutionCategories} services={serviceCategories} sectionAnchors={sectionAnchors} />
|
||||
<CategoryView client:load badge={badge} title={title} description={description} vendors={vendors} contacts={contacts} />
|
||||
<Footer
|
||||
client:load
|
||||
|
||||
@@ -9,6 +9,7 @@ import { menuItemsQuery } from '../graphql-documents/menu-items.gql.generated'
|
||||
import { homePageQuery } from '../graphql-documents/home-page.gql.generated'
|
||||
import { siteConfigQuery } from '../graphql-documents/site-config.gql.generated'
|
||||
import { resolveUploadUrl } from '../lib/cms/uploads/url'
|
||||
import { getHomeSectionAnchors } from '../lib/cms/sections/homeNav'
|
||||
|
||||
const [homeResult, siteResult, menuItemsResult] = await Promise.all([
|
||||
homePageQuery.execute(),
|
||||
@@ -39,11 +40,12 @@ const menuItems = (menuItemsResult.data?.categories?.filter(c => c != null) ?? [
|
||||
|
||||
const solutionCategories = menuItems.filter(c => c.group === Enum_Category_Group.Solution)
|
||||
const serviceCategories = menuItems.filter(c => c.group === Enum_Category_Group.Service)
|
||||
const sectionAnchors = getHomeSectionAnchors(homePage)
|
||||
---
|
||||
|
||||
<Layout title={siteTitle} description={siteDescription}>
|
||||
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
||||
<Header client:load siteTitle={siteTitle} contacts={contacts} solutions={solutionCategories} services={serviceCategories} />
|
||||
<Header client:load siteTitle={siteTitle} contacts={contacts} solutions={solutionCategories} services={serviceCategories} sectionAnchors={sectionAnchors} />
|
||||
<HomePage client:load homePage={homePage} contacts={contacts} />
|
||||
<Footer
|
||||
client:load
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Menu, X, ChevronDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { ContactModal } from '../components/ContactModal'
|
||||
import type { MenuItemsQuery } from '../graphql/graphql'
|
||||
import type { HomeSectionAnchors } from '../lib/cms/sections/homeNav'
|
||||
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
||||
import { navigateHomeSectionAnchor } from '../lib/scrollToHash'
|
||||
|
||||
@@ -12,9 +13,16 @@ export interface HeaderProps {
|
||||
contacts?: readonly SiteConfigContact[]
|
||||
solutions?: readonly CategoryNavItem[]
|
||||
services?: readonly CategoryNavItem[]
|
||||
sectionAnchors?: HomeSectionAnchors
|
||||
}
|
||||
|
||||
export function Header({ siteTitle = '', contacts = [], solutions = [], services = [] }: HeaderProps) {
|
||||
export function Header({
|
||||
siteTitle = '',
|
||||
contacts = [],
|
||||
solutions = [],
|
||||
services = [],
|
||||
sectionAnchors = {},
|
||||
}: HeaderProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [isSolutionsOpen, setIsSolutionsOpen] = useState(false)
|
||||
const [isServicesOpen, setIsServicesOpen] = useState(false)
|
||||
@@ -86,20 +94,24 @@ export function Header({ siteTitle = '', contacts = [], solutions = [], services
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<a
|
||||
href="/#cases"
|
||||
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
||||
onClick={e => navigateHomeSectionAnchor(e, 'cases')}
|
||||
>
|
||||
Кейсы
|
||||
</a>
|
||||
<a
|
||||
href="/#about"
|
||||
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
||||
onClick={e => navigateHomeSectionAnchor(e, 'about')}
|
||||
>
|
||||
О компании
|
||||
</a>
|
||||
{sectionAnchors.cases && (
|
||||
<a
|
||||
href={`/#${sectionAnchors.cases}`}
|
||||
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
||||
onClick={e => navigateHomeSectionAnchor(e, sectionAnchors.cases!)}
|
||||
>
|
||||
Кейсы
|
||||
</a>
|
||||
)}
|
||||
{sectionAnchors.about && (
|
||||
<a
|
||||
href={`/#${sectionAnchors.about}`}
|
||||
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
||||
onClick={e => navigateHomeSectionAnchor(e, sectionAnchors.about!)}
|
||||
>
|
||||
О компании
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href="/#contact"
|
||||
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
|
||||
@@ -185,26 +197,30 @@ export function Header({ siteTitle = '', contacts = [], solutions = [], services
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<a
|
||||
href="/#cases"
|
||||
onClick={e => {
|
||||
navigateHomeSectionAnchor(e, 'cases')
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
className="text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
Кейсы
|
||||
</a>
|
||||
<a
|
||||
href="/#about"
|
||||
onClick={e => {
|
||||
navigateHomeSectionAnchor(e, 'about')
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
className="text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
О компании
|
||||
</a>
|
||||
{sectionAnchors.cases && (
|
||||
<a
|
||||
href={`/#${sectionAnchors.cases}`}
|
||||
onClick={e => {
|
||||
navigateHomeSectionAnchor(e, sectionAnchors.cases!)
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
className="text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
Кейсы
|
||||
</a>
|
||||
)}
|
||||
{sectionAnchors.about && (
|
||||
<a
|
||||
href={`/#${sectionAnchors.about}`}
|
||||
onClick={e => {
|
||||
navigateHomeSectionAnchor(e, sectionAnchors.about!)
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
className="text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
О компании
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href="/#contact"
|
||||
onClick={e => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import type { HomePageQuery } from '../graphql/graphql'
|
||||
import { Hero } from '../sections/Hero'
|
||||
import { Services } from '../sections/Services'
|
||||
@@ -56,6 +56,17 @@ function parseHomeSections(home: Maybe<HomePageData>) {
|
||||
return { hero, services, solutions, cases, about }
|
||||
}
|
||||
|
||||
/** Wraps a section in an anchor target (id + header-offset scroll margin) when it has an anchor. */
|
||||
function SectionAnchor({ anchor, children }: { anchor?: Maybe<string>; children: ReactNode }) {
|
||||
const id = anchor?.trim()
|
||||
if (!id) return <>{children}</>
|
||||
return (
|
||||
<div id={id} style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface HomePageProps {
|
||||
homePage?: Maybe<HomePageData>
|
||||
contacts?: readonly SiteConfigContact[]
|
||||
@@ -75,15 +86,21 @@ export function HomePage({ homePage, contacts = [] }: HomePageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero onOpenContactModal={() => setContactOpen(true)} data={hero} />
|
||||
<Services data={services} />
|
||||
<Solutions data={solutions} />
|
||||
<div id="cases" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||
<SectionAnchor anchor={hero?.anchor}>
|
||||
<Hero onOpenContactModal={() => setContactOpen(true)} data={hero} />
|
||||
</SectionAnchor>
|
||||
<SectionAnchor anchor={services?.anchor}>
|
||||
<Services data={services} />
|
||||
</SectionAnchor>
|
||||
<SectionAnchor anchor={solutions?.anchor}>
|
||||
<Solutions data={solutions} />
|
||||
</SectionAnchor>
|
||||
<SectionAnchor anchor={cases?.anchor}>
|
||||
<Cases data={cases} />
|
||||
</div>
|
||||
<div id="about" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||
</SectionAnchor>
|
||||
<SectionAnchor anchor={about?.anchor}>
|
||||
<About data={about} />
|
||||
</div>
|
||||
</SectionAnchor>
|
||||
<div id="contact" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||
<Contact contacts={contacts} submissionSource="home_contact_section" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user