feat: add contacts from cms
This commit is contained in:
@@ -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) {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Контакты компактно */}
|
||||
<div className="mt-8 pt-8 border-t border-gray-100 flex flex-wrap justify-center gap-6 text-sm text-gray-600">
|
||||
<a href="tel:+74951234567" className="flex items-center gap-2 hover:text-blue-600 transition-colors">
|
||||
<Phone className="w-4 h-4" />
|
||||
<span>+7 (495) 123-45-67</span>
|
||||
</a>
|
||||
<a
|
||||
href="mailto:info@softclick.ru"
|
||||
className="flex items-center gap-2 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
<span>info@softclick.ru</span>
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>Москва, Пресненская наб., 12</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Пн-Пт 9:00–18:00</span>
|
||||
</div>
|
||||
<div className="mt-8 pt-8 border-t border-gray-100">
|
||||
<SiteContactItems contacts={contacts} variant="inline" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
82
src/components/SiteContactItems.tsx
Normal file
82
src/components/SiteContactItems.tsx
Normal file
@@ -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 (
|
||||
<ul className={className ?? 'space-y-4'}>
|
||||
{contacts.map(c => {
|
||||
const Icon = toIcon(c.type)
|
||||
const href = hrefFor(c)
|
||||
const label = labelFor(c)
|
||||
const content = href ? (
|
||||
<a href={href} className="hover:text-blue-400 transition-colors">
|
||||
{label}
|
||||
</a>
|
||||
) : (
|
||||
<span>{label}</span>
|
||||
)
|
||||
return (
|
||||
<li key={c.id} className="flex items-start gap-3">
|
||||
<Icon className="w-5 h-5 mt-1 shrink-0 text-blue-400" />
|
||||
<div>{content}</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
const rowClass = className ?? 'flex flex-wrap justify-center gap-6 text-sm text-gray-600'
|
||||
|
||||
return (
|
||||
<div className={rowClass}>
|
||||
{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 (
|
||||
<a key={c.id} href={href} className={linkClass}>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key={c.id} className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
5
src/lib/cms/contact/types.ts
Normal file
5
src/lib/cms/contact/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { SiteConfigQueryQuery } from '../../../graphql/graphql'
|
||||
|
||||
export type SiteConfigContact = NonNullable<
|
||||
NonNullable<NonNullable<SiteConfigQueryQuery['siteConfig']>['contacts']>[number]
|
||||
>
|
||||
@@ -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
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
||||
<Header client:load />
|
||||
<Header client:load contacts={contacts} />
|
||||
<CategoryView client:load title={title} description={description} vendors={vendors} />
|
||||
<Footer client:load />
|
||||
<Footer client:load contacts={contacts} />
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -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) ?? []
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="min-h-screen bg-white w-full overflow-x-hidden">
|
||||
<Header client:load />
|
||||
<HomePage client:load homePage={homePage} />
|
||||
<Footer client:load />
|
||||
<Header client:load contacts={contacts} />
|
||||
<HomePage client:load homePage={homePage} contacts={contacts} />
|
||||
<Footer client:load contacts={contacts} />
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -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 (
|
||||
<section className="py-4 bg-gray-50">
|
||||
<div className="max-w-225 mx-auto px-6">
|
||||
@@ -77,25 +82,11 @@ export function Contact() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Контакты компактно */}
|
||||
<div className="mt-12 flex flex-wrap justify-center gap-8 text-sm text-gray-600">
|
||||
<a href="tel:+74951234567" className="flex items-center gap-2 hover:text-blue-600 transition-colors">
|
||||
<Phone className="w-4 h-4" />
|
||||
<span>+7 (495) 123-45-67</span>
|
||||
</a>
|
||||
<a href="mailto:info@softclick.ru" className="flex items-center gap-2 hover:text-blue-600 transition-colors">
|
||||
<Mail className="w-4 h-4" />
|
||||
<span>info@softclick.ru</span>
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>Москва, Пресненская наб., 12</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Пн-Пт 9:00–18:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<SiteContactItems
|
||||
contacts={contacts}
|
||||
variant="inline"
|
||||
className="mt-12 flex flex-wrap justify-center gap-8 text-sm text-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<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">
|
||||
@@ -103,25 +110,7 @@ export function Footer() {
|
||||
|
||||
<div>
|
||||
<h4 className="text-white mb-6">Контакты</h4>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<Phone className="w-5 h-5 mt-1 shrink-0 text-blue-400" />
|
||||
<div>
|
||||
<div className="text-white">+7 (495) 123-45-67</div>
|
||||
<div>Пн-Пт 9:00-18:00</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Mail className="w-5 h-5 mt-1 shrink-0 text-blue-400" />
|
||||
<a href="mailto:info@softclick.ru" className="hover:text-blue-400 transition-colors">
|
||||
info@softclick.ru
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin className="w-5 h-5 mt-1 shrink-0 text-blue-400" />
|
||||
<div>Москва, Пресненская наб., 12</div>
|
||||
</li>
|
||||
</ul>
|
||||
<SiteContactItems contacts={contacts} variant="footer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Menu, X, ChevronDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { ContactModal } from '../components/ContactModal'
|
||||
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
||||
import { navigateHomeSectionAnchor } from '../lib/scrollToHash'
|
||||
|
||||
export function Header() {
|
||||
export interface HeaderProps {
|
||||
contacts: readonly SiteConfigContact[]
|
||||
}
|
||||
|
||||
export function Header({ contacts }: HeaderProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [isSolutionsOpen, setIsSolutionsOpen] = useState(false)
|
||||
const [isServicesOpen, setIsServicesOpen] = useState(false)
|
||||
@@ -223,7 +228,11 @@ export function Header() {
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
<ContactModal isOpen={isContactModalOpen} onClose={() => setIsContactModalOpen(false)} />
|
||||
<ContactModal
|
||||
isOpen={isContactModalOpen}
|
||||
onClose={() => setIsContactModalOpen(false)}
|
||||
contacts={contacts}
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Cases } from '../sections/Cases'
|
||||
import { About } from '../sections/About'
|
||||
import { Contact } from '../sections/Contact'
|
||||
import { ContactModal } from '../components/ContactModal'
|
||||
import type { SiteConfigContact } from '../lib/cms/contact/types'
|
||||
import { scrollToHashTarget } from '../lib/scrollToHash'
|
||||
import type { Maybe } from '../lib/types'
|
||||
|
||||
@@ -57,9 +58,10 @@ function parseHomeSections(home: Maybe<HomePageData>) {
|
||||
|
||||
export interface HomePageProps {
|
||||
homePage?: Maybe<HomePageData>
|
||||
contacts: readonly SiteConfigContact[]
|
||||
}
|
||||
|
||||
export function HomePage({ homePage }: HomePageProps) {
|
||||
export function HomePage({ homePage, contacts }: HomePageProps) {
|
||||
const [contactOpen, setContactOpen] = useState(false)
|
||||
const { hero, services, solutions, cases, about } = parseHomeSections(homePage ?? null)
|
||||
|
||||
@@ -83,9 +85,9 @@ export function HomePage({ homePage }: HomePageProps) {
|
||||
<About data={about} />
|
||||
</div>
|
||||
<div id="contact" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||
<Contact />
|
||||
<Contact contacts={contacts} />
|
||||
</div>
|
||||
<ContactModal isOpen={contactOpen} onClose={() => setContactOpen(false)} />
|
||||
<ContactModal isOpen={contactOpen} onClose={() => setContactOpen(false)} contacts={contacts} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user