From 32ea9359aec8d190b1b769a93fc5b3487b349e8f Mon Sep 17 00:00:00 2001 From: Zotov Ivan Yuryevich Date: Tue, 14 Apr 2026 22:55:10 +0300 Subject: [PATCH] feat: add contacts from cms --- src/components/ContactModal.tsx | 31 +++-------- src/components/SiteContactItems.tsx | 82 +++++++++++++++++++++++++++++ src/lib/cms/contact/types.ts | 5 ++ src/pages/[slug].astro | 16 ++++-- src/pages/index.astro | 23 +++++--- src/sections/Contact.tsx | 33 +++++------- src/sections/Footer.tsx | 31 ++++------- src/sections/Header.tsx | 13 ++++- src/views/HomePage.tsx | 8 +-- 9 files changed, 162 insertions(+), 80 deletions(-) create mode 100644 src/components/SiteContactItems.tsx create mode 100644 src/lib/cms/contact/types.ts diff --git a/src/components/ContactModal.tsx b/src/components/ContactModal.tsx index cf42de3..b08d101 100644 --- a/src/components/ContactModal.tsx +++ b/src/components/ContactModal.tsx @@ -1,13 +1,17 @@ -import { X, Mail, Phone, MapPin, Clock, Calendar } from 'lucide-react' +import { X, Calendar } from 'lucide-react' import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' +import { SiteContactItems } from './SiteContactItems' +import type { SiteConfigContact } from '../lib/cms/contact/types' + interface ContactModalProps { isOpen: boolean onClose: () => void + contacts: readonly SiteConfigContact[] } -export function ContactModal({ isOpen, onClose }: ContactModalProps) { +export function ContactModal({ isOpen, onClose, contacts }: ContactModalProps) { useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden' @@ -198,27 +202,8 @@ export function ContactModal({ isOpen, onClose }: ContactModalProps) { - {/* Контакты компактно */} -
- - - +7 (495) 123-45-67 - - - - info@softclick.ru - -
- - Москва, Пресненская наб., 12 -
-
- - Пн-Пт 9:00–18:00 -
+
+
diff --git a/src/components/SiteContactItems.tsx b/src/components/SiteContactItems.tsx new file mode 100644 index 0000000..f7b1b8c --- /dev/null +++ b/src/components/SiteContactItems.tsx @@ -0,0 +1,82 @@ +import { Enum_Componentsharedcontact_Type } from '../graphql/graphql' +import { toIcon } from '../lib/cms/contact/toIcon' +import type { SiteConfigContact } from '../lib/cms/contact/types' + +function hrefFor(contact: SiteConfigContact): string | undefined { + switch (contact.type) { + case Enum_Componentsharedcontact_Type.Phone: + return `tel:${contact.value.replace(/\s/g, '')}` + case Enum_Componentsharedcontact_Type.Email: + return `mailto:${contact.value}` + default: + return undefined + } +} + +function labelFor(contact: SiteConfigContact): string { + const d = contact.displayValue?.trim() + return d || contact.value +} + +export interface SiteContactItemsProps { + contacts: readonly SiteConfigContact[] + variant: 'inline' | 'footer' + /** Applied to the outer wrapper (inline: flex row; footer: ul gets section spacing from parent) */ + className?: string +} + +export function SiteContactItems({ contacts, variant, className }: SiteContactItemsProps) { + if (!contacts.length) return null + + if (variant === 'footer') { + return ( + + ) + } + + const rowClass = className ?? 'flex flex-wrap justify-center gap-6 text-sm text-gray-600' + + return ( +
+ {contacts.map(c => { + const Icon = toIcon(c.type) + const href = hrefFor(c) + const label = labelFor(c) + const linkClass = 'flex items-center gap-2 hover:text-blue-600 transition-colors' + if (href) { + return ( + + + {label} + + ) + } + return ( +
+ + {label} +
+ ) + })} +
+ ) +} diff --git a/src/lib/cms/contact/types.ts b/src/lib/cms/contact/types.ts new file mode 100644 index 0000000..f8c3ede --- /dev/null +++ b/src/lib/cms/contact/types.ts @@ -0,0 +1,5 @@ +import type { SiteConfigQueryQuery } from '../../../graphql/graphql' + +export type SiteConfigContact = NonNullable< + NonNullable['contacts']>[number] +> diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro index cfa435b..70e30c6 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -5,15 +5,24 @@ import { getCollection, type CollectionEntry } from 'astro:content' import { Header } from '../sections/Header' import { Footer } from '../sections/Footer' import { CategoryView } from '../views/CategoryView' +import type { SiteConfigContact } from '../lib/cms/contact/types' +import { siteConfigQueryQuery } from '../graphql-documents/site-config.gql.generated' export async function getStaticPaths() { const categories = await getCollection('categories') + const siteResult = await siteConfigQueryQuery.execute() + if (siteResult.errors?.length) { + throw new Error(siteResult.errors.map(e => e.message).join(', ')) + } + const contacts = siteResult.data?.siteConfig?.contacts?.filter(c => c != null) ?? [] + return categories.map((entry: CollectionEntry<'categories'>) => ({ params: { slug: entry.id }, props: { title: entry.data.title, description: entry.data.description, vendors: entry.data.vendors, + contacts, }, })) } @@ -26,15 +35,16 @@ interface Props { description: string products?: string[] }[] + contacts: SiteConfigContact[] } -const { title, description, vendors } = Astro.props as Props +const { title, description, vendors, contacts } = Astro.props as Props ---
-
+
-
+
diff --git a/src/pages/index.astro b/src/pages/index.astro index 6c91ab8..8f92c3b 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -5,19 +5,28 @@ import { Header } from '../sections/Header' import { Footer } from '../sections/Footer' import { HomePage } from '../views/HomePage' import { homePageQueryQuery } from '../graphql-documents/home-page.gql.generated' +import { siteConfigQueryQuery } from '../graphql-documents/site-config.gql.generated' -const result = await homePageQueryQuery.execute() -if (result.errors?.length) { - throw new Error(result.errors.map(e => e.message).join(', ')) +const [homeResult, siteResult] = await Promise.all([ + homePageQueryQuery.execute(), + siteConfigQueryQuery.execute(), +]) + +if (homeResult.errors?.length) { + throw new Error(homeResult.errors.map(e => e.message).join(', ')) +} +if (siteResult.errors?.length) { + throw new Error(siteResult.errors.map(e => e.message).join(', ')) } -const homePage = result.data?.homePage ?? null +const homePage = homeResult.data?.homePage ?? null +const contacts = siteResult.data?.siteConfig?.contacts?.filter(c => c != null) ?? [] ---
-
- -
+
+ +
diff --git a/src/sections/Contact.tsx b/src/sections/Contact.tsx index 320ab15..de9d4b1 100644 --- a/src/sections/Contact.tsx +++ b/src/sections/Contact.tsx @@ -1,6 +1,11 @@ -import { Phone, Mail, MapPin, Clock } from 'lucide-react' +import { SiteContactItems } from '../components/SiteContactItems' +import type { SiteConfigContact } from '../lib/cms/contact/types' -export function Contact() { +export interface ContactProps { + contacts: readonly SiteConfigContact[] +} + +export function Contact({ contacts }: ContactProps) { return (
@@ -77,25 +82,11 @@ export function Contact() {
- {/* Контакты компактно */} -
- - - +7 (495) 123-45-67 - - - - info@softclick.ru - -
- - Москва, Пресненская наб., 12 -
-
- - Пн-Пт 9:00–18:00 -
-
+
) diff --git a/src/sections/Footer.tsx b/src/sections/Footer.tsx index e9374c1..f1f0192 100644 --- a/src/sections/Footer.tsx +++ b/src/sections/Footer.tsx @@ -1,6 +1,13 @@ -import { Mail, Phone, MapPin, Send, Hash, Code, Video } from 'lucide-react' +import { Send, Video } from 'lucide-react' -export function Footer() { +import { SiteContactItems } from '../components/SiteContactItems' +import type { SiteConfigContact } from '../lib/cms/contact/types' + +export interface FooterProps { + contacts: readonly SiteConfigContact[] +} + +export function Footer({ contacts }: FooterProps) { return (