fetch sections from cms
This commit is contained in:
@@ -3,11 +3,11 @@
|
|||||||
import type { ExecutionResult } from "graphql";
|
import type { ExecutionResult } from "graphql";
|
||||||
|
|
||||||
import { execute } from "../graphql/execute";
|
import { execute } from "../graphql/execute";
|
||||||
import type { HomePageQuery, HomePageQueryVariables } from "../graphql/graphql";
|
import type { HomePageQueryQuery, HomePageQueryQueryVariables } from "../graphql/graphql";
|
||||||
import { homePageDocument } from "./home-page.gql";
|
import { homePageDocument } from "./home-page.gql";
|
||||||
|
|
||||||
export const homePageQuery = {
|
export const homePageQueryQuery = {
|
||||||
async execute(variables?: HomePageQueryVariables): Promise<ExecutionResult<HomePageQuery>> {
|
async execute(variables?: HomePageQueryQueryVariables): Promise<ExecutionResult<HomePageQueryQuery>> {
|
||||||
return execute<HomePageQuery, HomePageQueryVariables>(homePageDocument, variables);
|
return execute<HomePageQueryQuery, HomePageQueryQueryVariables>(homePageDocument, variables);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const homePageDocument = gql`
|
|||||||
query HomePageQuery {
|
query HomePageQuery {
|
||||||
homePage {
|
homePage {
|
||||||
sections {
|
sections {
|
||||||
|
__typename
|
||||||
... on ComponentSectionsHomeHero {
|
... on ComponentSectionsHomeHero {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
@@ -42,6 +43,7 @@ export const homePageDocument = gql`
|
|||||||
id
|
id
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
|
footnote
|
||||||
icon {
|
icon {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,7 @@ export const homePageDocument = gql`
|
|||||||
description
|
description
|
||||||
pretitle
|
pretitle
|
||||||
badge
|
badge
|
||||||
|
footnote
|
||||||
color {
|
color {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1539,10 +1539,10 @@ export type HomePageQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
|||||||
|
|
||||||
export type HomePageQueryQuery = { __typename?: 'Query', homePage?: { __typename?: 'HomePage', sections?: Array<
|
export type HomePageQueryQuery = { __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?: '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, 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?: '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?: '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, icon?: { __typename?: 'ComponentSharedIcon', value?: Enum_Componentsharedicon_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?: 'Error' }
|
| { __typename?: 'Error' }
|
||||||
| null> | null } | null };
|
| null> | null } | null };
|
||||||
|
|
||||||
@@ -1619,6 +1619,7 @@ export const HomePageQueryDocument = new TypedDocumentString(`
|
|||||||
id
|
id
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
|
footnote
|
||||||
icon {
|
icon {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
@@ -1637,6 +1638,7 @@ export const HomePageQueryDocument = new TypedDocumentString(`
|
|||||||
description
|
description
|
||||||
pretitle
|
pretitle
|
||||||
badge
|
badge
|
||||||
|
footnote
|
||||||
color {
|
color {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/lib/cms/color/index.ts
Normal file
4
src/lib/cms/color/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { toBackground } from './toBackground'
|
||||||
|
export { toElevatedGradient } from './toElevatedGradient'
|
||||||
|
export { toGradient } from './toGradient'
|
||||||
|
export { toTextColor } from './toTextColor'
|
||||||
21
src/lib/cms/color/toBackground.ts
Normal file
21
src/lib/cms/color/toBackground.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Enum_Componentsharedcolor_Value } from '../../../graphql/graphql'
|
||||||
|
import type { Maybe } from '../../types'
|
||||||
|
|
||||||
|
const DEFAULT_BACKGROUND = 'bg-blue-50'
|
||||||
|
|
||||||
|
const BACKGROUND_MAP: Record<Enum_Componentsharedcolor_Value, string> = {
|
||||||
|
[Enum_Componentsharedcolor_Value.Blue]: 'bg-blue-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Cyan]: 'bg-cyan-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Emerald]: 'bg-emerald-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Indigo]: 'bg-indigo-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Orange]: 'bg-orange-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Pink]: 'bg-pink-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Violet]: 'bg-violet-50',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBackground(color: Maybe<Enum_Componentsharedcolor_Value>): string {
|
||||||
|
if (color != null && color in BACKGROUND_MAP) {
|
||||||
|
return BACKGROUND_MAP[color]
|
||||||
|
}
|
||||||
|
return DEFAULT_BACKGROUND
|
||||||
|
}
|
||||||
21
src/lib/cms/color/toElevatedGradient.ts
Normal file
21
src/lib/cms/color/toElevatedGradient.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Enum_Componentsharedcolor_Value } from '../../../graphql/graphql'
|
||||||
|
import type { Maybe } from '../../types'
|
||||||
|
|
||||||
|
const DEFAULT_ELEVATED_GRADIENT = 'from-blue-100 to-cyan-100'
|
||||||
|
|
||||||
|
const ELEVATED_GRADIENT_MAP: Record<Enum_Componentsharedcolor_Value, string> = {
|
||||||
|
[Enum_Componentsharedcolor_Value.Blue]: 'from-blue-100 to-cyan-100',
|
||||||
|
[Enum_Componentsharedcolor_Value.Cyan]: 'from-cyan-100 to-blue-100',
|
||||||
|
[Enum_Componentsharedcolor_Value.Emerald]: 'from-emerald-100 to-teal-100',
|
||||||
|
[Enum_Componentsharedcolor_Value.Indigo]: 'from-indigo-100 to-blue-100',
|
||||||
|
[Enum_Componentsharedcolor_Value.Orange]: 'from-orange-100 to-amber-100',
|
||||||
|
[Enum_Componentsharedcolor_Value.Pink]: 'from-pink-100 to-rose-100',
|
||||||
|
[Enum_Componentsharedcolor_Value.Violet]: 'from-violet-100 to-purple-100',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toElevatedGradient(color: Maybe<Enum_Componentsharedcolor_Value>): string {
|
||||||
|
if (color != null && color in ELEVATED_GRADIENT_MAP) {
|
||||||
|
return ELEVATED_GRADIENT_MAP[color]
|
||||||
|
}
|
||||||
|
return DEFAULT_ELEVATED_GRADIENT
|
||||||
|
}
|
||||||
21
src/lib/cms/color/toGradient.ts
Normal file
21
src/lib/cms/color/toGradient.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Enum_Componentsharedcolor_Value } from '../../../graphql/graphql'
|
||||||
|
import type { Maybe } from '../../types'
|
||||||
|
|
||||||
|
const DEFAULT_GRADIENT = 'from-blue-50 to-cyan-50'
|
||||||
|
|
||||||
|
const GRADIENT_MAP: Record<Enum_Componentsharedcolor_Value, string> = {
|
||||||
|
[Enum_Componentsharedcolor_Value.Blue]: 'from-blue-50 to-cyan-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Cyan]: 'from-cyan-50 to-blue-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Emerald]: 'from-emerald-50 to-teal-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Indigo]: 'from-indigo-50 to-blue-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Orange]: 'from-orange-50 to-amber-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Pink]: 'from-pink-50 to-rose-50',
|
||||||
|
[Enum_Componentsharedcolor_Value.Violet]: 'from-violet-50 to-purple-50',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toGradient(color: Maybe<Enum_Componentsharedcolor_Value>): string {
|
||||||
|
if (color != null && color in GRADIENT_MAP) {
|
||||||
|
return GRADIENT_MAP[color]
|
||||||
|
}
|
||||||
|
return DEFAULT_GRADIENT
|
||||||
|
}
|
||||||
21
src/lib/cms/color/toTextColor.ts
Normal file
21
src/lib/cms/color/toTextColor.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Enum_Componentsharedcolor_Value } from '../../../graphql/graphql'
|
||||||
|
import type { Maybe } from '../../types'
|
||||||
|
|
||||||
|
const DEFAULT_TEXT_COLOR = 'text-blue-600'
|
||||||
|
|
||||||
|
const TEXT_COLOR_MAP: Record<Enum_Componentsharedcolor_Value, string> = {
|
||||||
|
[Enum_Componentsharedcolor_Value.Blue]: 'text-blue-600',
|
||||||
|
[Enum_Componentsharedcolor_Value.Cyan]: 'text-cyan-600',
|
||||||
|
[Enum_Componentsharedcolor_Value.Emerald]: 'text-emerald-600',
|
||||||
|
[Enum_Componentsharedcolor_Value.Indigo]: 'text-indigo-600',
|
||||||
|
[Enum_Componentsharedcolor_Value.Orange]: 'text-orange-600',
|
||||||
|
[Enum_Componentsharedcolor_Value.Pink]: 'text-pink-600',
|
||||||
|
[Enum_Componentsharedcolor_Value.Violet]: 'text-violet-600',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTextColor(color: Maybe<Enum_Componentsharedcolor_Value>): string {
|
||||||
|
if (color != null && color in TEXT_COLOR_MAP) {
|
||||||
|
return TEXT_COLOR_MAP[color]
|
||||||
|
}
|
||||||
|
return DEFAULT_TEXT_COLOR
|
||||||
|
}
|
||||||
1
src/lib/cms/icon/index.ts
Normal file
1
src/lib/cms/icon/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { toComponent } from './toComponent'
|
||||||
54
src/lib/cms/icon/toComponent.ts
Normal file
54
src/lib/cms/icon/toComponent.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { LucideIcon } from 'lucide-react'
|
||||||
|
import {
|
||||||
|
Award,
|
||||||
|
Building2,
|
||||||
|
CheckCircle2,
|
||||||
|
Cloud,
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
Factory,
|
||||||
|
GraduationCap,
|
||||||
|
Heart,
|
||||||
|
Landmark,
|
||||||
|
Package,
|
||||||
|
Rocket,
|
||||||
|
Server,
|
||||||
|
Settings,
|
||||||
|
Shield,
|
||||||
|
ShoppingCart,
|
||||||
|
Users,
|
||||||
|
Zap,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import { Enum_Componentsharedicon_Value } from '../../../graphql/graphql'
|
||||||
|
import type { Maybe } from '../../types'
|
||||||
|
|
||||||
|
const DEFAULT_ICON = Cloud
|
||||||
|
|
||||||
|
const ICON_MAP: Record<Enum_Componentsharedicon_Value, LucideIcon> = {
|
||||||
|
[Enum_Componentsharedicon_Value.Award]: Award,
|
||||||
|
[Enum_Componentsharedicon_Value.Building]: Building2,
|
||||||
|
[Enum_Componentsharedicon_Value.CheckCircle]: CheckCircle2,
|
||||||
|
[Enum_Componentsharedicon_Value.Cloud]: Cloud,
|
||||||
|
[Enum_Componentsharedicon_Value.Cpu]: Cpu,
|
||||||
|
[Enum_Componentsharedicon_Value.Database]: Database,
|
||||||
|
[Enum_Componentsharedicon_Value.Factory]: Factory,
|
||||||
|
[Enum_Componentsharedicon_Value.GraduationCap]: GraduationCap,
|
||||||
|
[Enum_Componentsharedicon_Value.Heart]: Heart,
|
||||||
|
[Enum_Componentsharedicon_Value.Landmark]: Landmark,
|
||||||
|
[Enum_Componentsharedicon_Value.Package]: Package,
|
||||||
|
[Enum_Componentsharedicon_Value.Rocket]: Rocket,
|
||||||
|
[Enum_Componentsharedicon_Value.Server]: Server,
|
||||||
|
[Enum_Componentsharedicon_Value.Settings]: Settings,
|
||||||
|
[Enum_Componentsharedicon_Value.Shield]: Shield,
|
||||||
|
[Enum_Componentsharedicon_Value.ShoppingCart]: ShoppingCart,
|
||||||
|
[Enum_Componentsharedicon_Value.Users]: Users,
|
||||||
|
[Enum_Componentsharedicon_Value.Zap]: Zap,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toComponent(value?: Maybe<Enum_Componentsharedicon_Value>): LucideIcon {
|
||||||
|
if (value != null && value in ICON_MAP) {
|
||||||
|
return ICON_MAP[value]
|
||||||
|
}
|
||||||
|
return DEFAULT_ICON
|
||||||
|
}
|
||||||
5
src/lib/types.ts
Normal file
5
src/lib/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Value that may be absent as `null` (e.g. GraphQL) or `undefined` (optional fields).
|
||||||
|
* Note: generated `src/graphql/graphql.ts` defines codegen `Maybe<T>` as `T | null` only.
|
||||||
|
*/
|
||||||
|
export type Maybe<T> = T | null | undefined
|
||||||
@@ -4,12 +4,20 @@ 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 { homePageQueryQuery } from '../graphql-documents/home-page.gql.generated'
|
||||||
|
|
||||||
|
const result = await homePageQueryQuery.execute()
|
||||||
|
if (result.errors?.length) {
|
||||||
|
throw new Error(result.errors.map(e => e.message).join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
|
const homePage = result.data?.homePage ?? 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 />
|
<Header client:load />
|
||||||
<HomePage client:load />
|
<HomePage client:load homePage={homePage} />
|
||||||
<Footer client:load />
|
<Footer client:load />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,177 +1,101 @@
|
|||||||
import {
|
|
||||||
CheckCircle,
|
|
||||||
Rocket,
|
|
||||||
Zap,
|
|
||||||
Target,
|
|
||||||
Award,
|
|
||||||
Shield,
|
|
||||||
Clock,
|
|
||||||
HeadphonesIcon,
|
|
||||||
TrendingUp,
|
|
||||||
Users,
|
|
||||||
CheckCircle2,
|
|
||||||
Cloud,
|
|
||||||
Lock,
|
|
||||||
Database,
|
|
||||||
Briefcase,
|
|
||||||
Server,
|
|
||||||
Cpu,
|
|
||||||
Package,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { toComponent } from '../lib/cms/icon'
|
||||||
|
import { toBackground, toTextColor } from '../lib/cms/color'
|
||||||
|
import type { Enum_Componentsharedcolor_Value, Enum_Componentsharedicon_Value } from '../graphql/graphql'
|
||||||
|
import type { Maybe } from '../lib/types'
|
||||||
|
|
||||||
const allVendors = [
|
export interface AboutCardCms {
|
||||||
'Яндекс.Облако',
|
id: string
|
||||||
'VK Cloud',
|
title?: Maybe<string>
|
||||||
'MTC Web Services',
|
description?: Maybe<string>
|
||||||
'Сбер Облако',
|
color?: Maybe<{ value?: Maybe<Enum_Componentsharedcolor_Value> }>
|
||||||
'Kaspersky',
|
icon?: Maybe<{ value?: Maybe<Enum_Componentsharedicon_Value> }>
|
||||||
'Positive Technologies',
|
}
|
||||||
'UserGate',
|
|
||||||
'Astra Linux',
|
export interface AboutCmsData {
|
||||||
'РедСофт',
|
title?: Maybe<string>
|
||||||
'Альт Линукс',
|
description?: Maybe<string>
|
||||||
'Postgres Pro',
|
partners?: Maybe<Array<Maybe<{ id: string; value: string }>>>
|
||||||
'Tarantool',
|
cards?: Maybe<Array<Maybe<AboutCardCms>>>
|
||||||
'Мой Офис',
|
}
|
||||||
'Р7-Офис',
|
|
||||||
'1С',
|
interface AboutProps {
|
||||||
'Галактика',
|
data?: Maybe<AboutCmsData>
|
||||||
'Kraftway',
|
}
|
||||||
'Aquarius',
|
|
||||||
'Эльбрус',
|
export function About({ data }: AboutProps) {
|
||||||
'Битрикс24',
|
const title = data?.title ?? ''
|
||||||
'Код Безопасности',
|
const description = data?.description ?? ''
|
||||||
'Zabbix',
|
const partners = (data?.partners ?? []).filter((p): p is { id: string; value: string } => Boolean(p?.value))
|
||||||
'Nginx',
|
const cards = (data?.cards ?? []).filter((c): c is AboutCardCms => Boolean(c))
|
||||||
'Selectel',
|
|
||||||
'Лаборатория Эльбрус',
|
|
||||||
'Basealt',
|
|
||||||
'МойОфис Почта',
|
|
||||||
'Видеомост',
|
|
||||||
'Jazz Telecom',
|
|
||||||
'Р-Видео',
|
|
||||||
'СКБ Контур',
|
|
||||||
'ЕТИС',
|
|
||||||
'Кодикс',
|
|
||||||
'Тензор',
|
|
||||||
'Infowatch',
|
|
||||||
'SearchInform',
|
|
||||||
'Гарда',
|
|
||||||
'Аладдин Р.Д.',
|
|
||||||
'С-Терра',
|
|
||||||
'DeviceLock',
|
|
||||||
'КриптоПро',
|
|
||||||
'Базальт СПО',
|
|
||||||
'Аскон',
|
|
||||||
'НаноСофт',
|
|
||||||
'Directum',
|
|
||||||
'Open Yard',
|
|
||||||
'ТруКонф',
|
|
||||||
'Yadro',
|
|
||||||
'Depo',
|
|
||||||
'Eltex',
|
|
||||||
'Qtech',
|
|
||||||
'D-Link',
|
|
||||||
]
|
|
||||||
|
|
||||||
export function About() {
|
|
||||||
const [currentVendorIndex, setCurrentVendorIndex] = useState(0)
|
const [currentVendorIndex, setCurrentVendorIndex] = useState(0)
|
||||||
const [isVisible, setIsVisible] = useState(true)
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (partners.length <= 1) return
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCurrentVendorIndex(prev => (prev + 1) % allVendors.length)
|
setCurrentVendorIndex(prev => (prev + 1) % partners.length)
|
||||||
setIsVisible(true)
|
setIsVisible(true)
|
||||||
}, 800)
|
}, 800)
|
||||||
}, 4500)
|
}, 4500)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [partners.length])
|
||||||
|
|
||||||
|
if (!title && !description && partners.length === 0 && cards.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-4 bg-gray-50">
|
<section className="py-4 bg-gray-50">
|
||||||
<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">
|
||||||
<div className="max-w-3xl mb-16">
|
<div className="max-w-3xl mb-16">
|
||||||
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">О компании</p>
|
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">О компании</p>
|
||||||
<h2 className="text-4xl lg:text-5xl text-gray-900 mb-6 flex flex-wrap items-baseline gap-2">
|
{title || partners.length > 0 ? (
|
||||||
<span>Сотрудничаем с</span>
|
<h2 className="text-4xl lg:text-5xl text-gray-900 mb-6 flex flex-wrap items-baseline gap-2">
|
||||||
<span
|
{title ? (
|
||||||
className="text-blue-600 transition-opacity duration-700 ease-in-out whitespace-nowrap"
|
<span>
|
||||||
style={{ opacity: isVisible ? 1 : 0 }}
|
{title}
|
||||||
>
|
{partners.length > 0 ? '\u00a0' : ''}
|
||||||
{allVendors[currentVendorIndex]}
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
</h2>
|
{partners.length > 0 ? (
|
||||||
<p className="text-xl text-gray-600">
|
<span
|
||||||
Комплексные решения для цифровой трансформации бизнеса. Работаем с 2021 года.
|
className="text-blue-600 transition-opacity duration-700 ease-in-out whitespace-nowrap"
|
||||||
</p>
|
style={{ opacity: isVisible ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
{partners[currentVendorIndex]?.value}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</h2>
|
||||||
|
) : null}
|
||||||
|
{description ? <p className="text-xl text-gray-600">{description}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ключевые преимущества для клиента */}
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all">
|
{cards.map(card => {
|
||||||
<div className="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center mb-6">
|
const Icon = toComponent(card.icon?.value)
|
||||||
<Package className="w-7 h-7 text-blue-600" />
|
const wrap = toBackground(card.color?.value)
|
||||||
</div>
|
const iconText = toTextColor(card.color?.value)
|
||||||
<h3 className="text-xl text-gray-900 mb-3">Поставка оборудования</h3>
|
|
||||||
<p className="text-gray-600 leading-relaxed">
|
|
||||||
Комплексные поставки серверов, СХД и сетевого оборудования от российских производителей.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all">
|
return (
|
||||||
<div className="w-14 h-14 bg-emerald-50 rounded-2xl flex items-center justify-center mb-6">
|
<div
|
||||||
<Award className="w-7 h-7 text-emerald-600" />
|
key={card.id}
|
||||||
</div>
|
className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all"
|
||||||
<h3 className="text-xl text-gray-900 mb-3">Официальное партнерство</h3>
|
>
|
||||||
<p className="text-gray-600 leading-relaxed">
|
<div className={`w-14 h-14 ${wrap} rounded-2xl flex items-center justify-center mb-6`}>
|
||||||
Статус золотого партнера ведущих российских вендоров. Прямые контракты и поддержка.
|
<Icon className={`w-7 h-7 ${iconText}`} />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
{card.title ? <h3 className="text-xl text-gray-900 mb-3">{card.title}</h3> : null}
|
||||||
|
{card.description ? <p className="text-gray-600 leading-relaxed">{card.description}</p> : null}
|
||||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all">
|
</div>
|
||||||
<div className="w-14 h-14 bg-violet-50 rounded-2xl flex items-center justify-center mb-6">
|
)
|
||||||
<CheckCircle2 className="w-7 h-7 text-violet-600" />
|
})}
|
||||||
</div>
|
|
||||||
<h3 className="text-xl text-gray-900 mb-3">Сертифицированные специалисты</h3>
|
|
||||||
<p className="text-gray-600 leading-relaxed">
|
|
||||||
Инженеры с подтвержденной квалификацией по всем внедряемым решениям и платформам.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all">
|
|
||||||
<div className="w-14 h-14 bg-orange-50 rounded-2xl flex items-center justify-center mb-6">
|
|
||||||
<Zap className="w-7 h-7 text-orange-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl text-gray-900 mb-3">Оперативный отклик</h3>
|
|
||||||
<p className="text-gray-600 leading-relaxed">
|
|
||||||
Быстрое реагирование на запросы клиента. Четкие сроки решения задач и прозрачная коммуникация.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all">
|
|
||||||
<div className="w-14 h-14 bg-cyan-50 rounded-2xl flex items-center justify-center mb-6">
|
|
||||||
<Users className="w-7 h-7 text-cyan-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl text-gray-900 mb-3">Выделенная команда</h3>
|
|
||||||
<p className="text-gray-600 leading-relaxed">
|
|
||||||
Персональный менеджер проекта и команда сертифицированных специалистов.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 hover:border-blue-200 hover:shadow-lg transition-all">
|
|
||||||
<div className="w-14 h-14 bg-pink-50 rounded-2xl flex items-center justify-center mb-6">
|
|
||||||
<Rocket className="w-7 h-7 text-pink-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl text-gray-900 mb-3">Быстрый старт</h3>
|
|
||||||
<p className="text-gray-600 leading-relaxed">
|
|
||||||
Запуск типовых проектов за 2 недели. Пилотное внедрение перед полным развертываием.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,85 +1,89 @@
|
|||||||
import { ArrowRight, Cloud, Cpu, Shield } from 'lucide-react'
|
import { ArrowRight } from 'lucide-react'
|
||||||
|
import { toComponent } from '../lib/cms/icon'
|
||||||
|
import { toElevatedGradient, toTextColor } from '../lib/cms/color'
|
||||||
|
import type { Enum_Componentsharedcolor_Value, Enum_Componentsharedicon_Value } from '../graphql/graphql'
|
||||||
|
import type { Maybe } from '../lib/types'
|
||||||
|
|
||||||
const cases = [
|
export interface CaseCardCms {
|
||||||
{
|
id: string
|
||||||
icon: Cloud,
|
title?: Maybe<string>
|
||||||
category: 'Облако',
|
description?: Maybe<string>
|
||||||
company: 'Торговая сеть',
|
pretitle?: Maybe<string>
|
||||||
title: 'Миграция в Яндекс.Облако',
|
badge?: Maybe<string>
|
||||||
description: 'Перенос корпоративной инфраструктуры 500+ серверов с нулевым простоем',
|
footnote?: Maybe<string>
|
||||||
results: '−40% TCO',
|
color?: Maybe<{ value?: Maybe<Enum_Componentsharedcolor_Value> }>
|
||||||
gradient: 'from-blue-100 to-cyan-100',
|
icon?: Maybe<{ value?: Maybe<Enum_Componentsharedicon_Value> }>
|
||||||
iconBg: 'bg-white',
|
}
|
||||||
iconColor: 'text-blue-600',
|
|
||||||
},
|
export interface CasesCmsData {
|
||||||
{
|
title?: Maybe<string>
|
||||||
icon: Cpu,
|
description?: Maybe<string>
|
||||||
category: 'Автоматизация',
|
cards?: Maybe<Array<Maybe<CaseCardCms>>>
|
||||||
company: 'Производственный холдинг',
|
}
|
||||||
title: 'Внедрение 1С:ERP',
|
|
||||||
description: 'Автоматизация производственного и финансового учета для 15 предприятий',
|
interface CasesProps {
|
||||||
results: '+25% эффективность',
|
data?: Maybe<CasesCmsData>
|
||||||
gradient: 'from-emerald-100 to-teal-100',
|
}
|
||||||
iconBg: 'bg-white',
|
|
||||||
iconColor: 'text-emerald-600',
|
export function Cases({ data }: CasesProps) {
|
||||||
},
|
const title = data?.title ?? ''
|
||||||
{
|
const description = data?.description ?? ''
|
||||||
icon: Shield,
|
const cards = (data?.cards ?? []).filter((c): c is CaseCardCms => Boolean(c))
|
||||||
category: 'Безопасность',
|
|
||||||
company: 'Финансовая группа',
|
if (!title && !description && cards.length === 0) {
|
||||||
title: 'Защита периметра Kaspersky',
|
return null
|
||||||
description: 'Построение многоуровневой системы защиты критичной инфраструктуры',
|
}
|
||||||
results: '99.9% uptime',
|
|
||||||
gradient: 'from-violet-100 to-purple-100',
|
|
||||||
iconBg: 'bg-white',
|
|
||||||
iconColor: 'text-violet-600',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function Cases() {
|
|
||||||
return (
|
return (
|
||||||
<section className="py-4 bg-white">
|
<section className="py-4 bg-white">
|
||||||
<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">
|
||||||
<div className="max-w-3xl mb-16">
|
<div className="max-w-3xl mb-16">
|
||||||
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">Кейсы</p>
|
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">Кейсы</p>
|
||||||
<h2 className="text-4xl lg:text-5xl text-gray-900 mb-6">Реализованные проекты</h2>
|
{title ? <h2 className="text-4xl lg:text-5xl text-gray-900 mb-6">{title}</h2> : null}
|
||||||
<p className="text-xl text-gray-600">Примеры успешного внедрения с конкретными результатами</p>
|
{description ? <p className="text-xl text-gray-600">{description}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-8">
|
<div className="grid lg:grid-cols-3 gap-8">
|
||||||
{cases.map((caseItem, index) => {
|
{cards.map((caseItem, index) => {
|
||||||
const Icon = caseItem.icon
|
const Icon = toComponent(caseItem.icon?.value)
|
||||||
|
const gradient = toElevatedGradient(caseItem.color?.value)
|
||||||
|
const iconColorClass = toTextColor(caseItem.color?.value)
|
||||||
const floatClass =
|
const floatClass =
|
||||||
index === 0 ? 'animate-float' : index === 1 ? 'animate-float-delay-1' : 'animate-float-delay-2'
|
index === 0 ? 'animate-float' : index === 1 ? 'animate-float-delay-1' : 'animate-float-delay-2'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={caseItem.id}
|
||||||
className="group bg-white rounded-2xl overflow-hidden border border-gray-100 hover:border-blue-200 hover:shadow-xl transition-all duration-300 cursor-pointer flex flex-col"
|
className="group bg-white rounded-2xl overflow-hidden border border-gray-100 hover:border-blue-200 hover:shadow-xl transition-all duration-300 cursor-pointer flex flex-col"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative h-64 overflow-hidden bg-linear-to-br ${caseItem.gradient} flex items-center justify-center`}
|
className={`relative h-64 overflow-hidden bg-linear-to-br ${gradient} flex items-center justify-center`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-24 h-24 ${caseItem.iconBg} rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-md ${floatClass}`}
|
className={`w-24 h-24 bg-white rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-md ${floatClass}`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-12 h-12 ${caseItem.iconColor}`} />
|
<Icon className={`w-12 h-12 ${iconColorClass}`} />
|
||||||
</div>
|
|
||||||
<div className="absolute top-4 left-4">
|
|
||||||
<span className="px-3 py-1 bg-white/90 backdrop-blur-sm text-gray-900 rounded-full border border-gray-200 text-sm">
|
|
||||||
{caseItem.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{caseItem.badge ? (
|
||||||
|
<div className="absolute top-4 left-4">
|
||||||
|
<span className="px-3 py-1 bg-white/90 backdrop-blur-sm text-gray-900 rounded-full border border-gray-200 text-sm">
|
||||||
|
{caseItem.badge}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 flex flex-col flex-1">
|
<div className="p-6 flex flex-col flex-1">
|
||||||
<div className="text-sm text-blue-600 mb-2">{caseItem.company}</div>
|
{caseItem.pretitle ? <div className="text-sm text-blue-600 mb-2">{caseItem.pretitle}</div> : null}
|
||||||
|
|
||||||
<h3 className="text-xl text-gray-900 mb-3">{caseItem.title}</h3>
|
{caseItem.title ? <h3 className="text-xl text-gray-900 mb-3">{caseItem.title}</h3> : null}
|
||||||
|
|
||||||
<p className="text-gray-600 mb-4 leading-relaxed flex-1">{caseItem.description}</p>
|
{caseItem.description ? (
|
||||||
|
<p className="text-gray-600 mb-4 leading-relaxed flex-1">{caseItem.description}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex items-center justify-between">
|
<div className="pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||||
<span className="text-gray-900">{caseItem.results}</span>
|
<span className="text-gray-900">{caseItem.footnote ?? ''}</span>
|
||||||
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all" />
|
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +93,10 @@ export function Cases() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mt-12">
|
<div className="text-center mt-12">
|
||||||
<button className="px-8 py-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all shadow-md hover:shadow-lg inline-flex items-center gap-2">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-8 py-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all shadow-md hover:shadow-lg inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
Все кейсы
|
Все кейсы
|
||||||
<ArrowRight className="w-5 h-5" />
|
<ArrowRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,58 +1,70 @@
|
|||||||
import { ArrowRight, Play, Cloud, Shield, Zap, Database, Code, Users } from 'lucide-react'
|
import { ArrowRight, Cloud, Shield, Zap, Database, Code, Users } from 'lucide-react'
|
||||||
|
import type { Maybe } from '../lib/types'
|
||||||
|
|
||||||
|
export interface HeroCmsData {
|
||||||
|
badge?: Maybe<string>
|
||||||
|
title?: Maybe<string>
|
||||||
|
description?: Maybe<string>
|
||||||
|
stats?: Maybe<Array<Maybe<{ id: string; key?: Maybe<string>; value?: Maybe<string> }>>>
|
||||||
|
}
|
||||||
|
|
||||||
interface HeroProps {
|
interface HeroProps {
|
||||||
onOpenContactModal?: () => void
|
onOpenContactModal?: () => void
|
||||||
|
data?: Maybe<HeroCmsData>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Hero({ onOpenContactModal }: HeroProps) {
|
export function Hero({ onOpenContactModal, data }: HeroProps) {
|
||||||
|
const badge = data?.badge ?? ''
|
||||||
|
const title = data?.title ?? ''
|
||||||
|
const description = data?.description ?? ''
|
||||||
|
const stats = (data?.stats ?? []).filter(Boolean) as Array<{ id: string; key?: Maybe<string>; value?: Maybe<string> }>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative pt-32 pb-20 overflow-hidden bg-white">
|
<section className="relative pt-32 pb-20 overflow-hidden bg-white">
|
||||||
<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">
|
||||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-700 rounded-full border border-blue-100">
|
{badge ? (
|
||||||
<span className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"></span>
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-700 rounded-full border border-blue-100">
|
||||||
<span>Надежный партнер с 2021 года</span>
|
<span className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"></span>
|
||||||
</div>
|
<span>{badge}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<h1 className="text-5xl lg:text-6xl text-gray-900 leading-tight">IT-решения для роста вашего бизнеса</h1>
|
{title ? <h1 className="text-5xl lg:text-6xl text-gray-900 leading-tight">{title}</h1> : null}
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 leading-relaxed">
|
{description ? <p className="text-xl text-gray-600 leading-relaxed">{description}</p> : null}
|
||||||
Проектируем, внедряем и сопровождаем корпоративные IT-системы. Облачная инфраструктура, безопасность,
|
|
||||||
автоматизация процессов.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<button
|
<button
|
||||||
className="group flex items-center gap-2 px-8 py-4 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
|
className="group flex items-center gap-2 px-8 py-4 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
|
||||||
onClick={onOpenContactModal}
|
onClick={onOpenContactModal}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Обсудить проект
|
Обсудить проект
|
||||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className="flex items-center gap-2 px-8 py-4 bg-gray-50 text-gray-900 rounded-xl hover:bg-gray-100 transition-all">
|
<a
|
||||||
|
href="#cases"
|
||||||
|
className="flex items-center gap-2 px-8 py-4 bg-gray-50 text-gray-900 rounded-xl hover:bg-gray-100 transition-all"
|
||||||
|
>
|
||||||
Портфолио
|
Портфолио
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-8 pt-8 border-t border-gray-100">
|
{stats.length > 0 ? (
|
||||||
<div>
|
<div className="grid grid-cols-3 gap-8 pt-8 border-t border-gray-100">
|
||||||
<div className="text-3xl text-gray-900 mb-1">300+</div>
|
{stats.map(stat => (
|
||||||
<div className="text-gray-600">клиентов</div>
|
<div key={stat.id}>
|
||||||
|
<div className="text-3xl text-gray-900 mb-1">{stat.value ?? ''}</div>
|
||||||
|
<div className="text-gray-600">{stat.key ?? ''}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
) : null}
|
||||||
<div className="text-3xl text-gray-900 mb-1">50+</div>
|
|
||||||
<div className="text-gray-600">партнерств</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-3xl text-gray-900 mb-1">95%</div>
|
|
||||||
<div className="text-gray-600">повторных обращений</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Простая сетка иконок в стиле VK/Яндекс */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes gentleFloat {
|
@keyframes gentleFloat {
|
||||||
|
|||||||
@@ -1,63 +1,50 @@
|
|||||||
import { Cloud, Shield, Cpu, Database, Server, Settings } from 'lucide-react'
|
import { toComponent } from '../lib/cms/icon'
|
||||||
|
import { toGradient, toTextColor } from '../lib/cms/color'
|
||||||
|
import type { Enum_Componentsharedcolor_Value, Enum_Componentsharedicon_Value } from '../graphql/graphql'
|
||||||
|
import type { Maybe } from '../lib/types'
|
||||||
|
|
||||||
const services = [
|
export interface ServiceCardCms {
|
||||||
{
|
id: string
|
||||||
icon: Cloud,
|
title?: Maybe<string>
|
||||||
title: 'Облачные решения',
|
description?: Maybe<string>
|
||||||
description: 'Миграция и управление инфраструктурой на базе Яндекс.Облако и VK Cloud',
|
icon?: Maybe<{ value?: Maybe<Enum_Componentsharedicon_Value> }>
|
||||||
gradient: 'from-blue-50 to-cyan-50',
|
color?: Maybe<{ value?: Maybe<Enum_Componentsharedcolor_Value> }>
|
||||||
iconColor: 'text-blue-600',
|
category?: Maybe<{ slug: string }>
|
||||||
},
|
}
|
||||||
{
|
|
||||||
icon: Shield,
|
export interface ServicesCmsData {
|
||||||
title: 'Кибербезопасность',
|
title?: Maybe<string>
|
||||||
description: 'Комплексная защита периметра и данных с решениями Kaspersky',
|
description?: Maybe<string>
|
||||||
gradient: 'from-violet-50 to-purple-50',
|
cards?: Maybe<Array<Maybe<ServiceCardCms>>>
|
||||||
iconColor: 'text-violet-600',
|
}
|
||||||
},
|
|
||||||
{
|
interface ServicesProps {
|
||||||
icon: Cpu,
|
data?: Maybe<ServicesCmsData>
|
||||||
title: 'Цифровая трансформация',
|
}
|
||||||
description: 'Автоматизация процессов на платформах 1С и Галактика',
|
|
||||||
gradient: 'from-emerald-50 to-teal-50',
|
export function Services({ data }: ServicesProps) {
|
||||||
iconColor: 'text-emerald-600',
|
const title = data?.title ?? ''
|
||||||
},
|
const description = data?.description ?? ''
|
||||||
{
|
const cards = (data?.cards ?? []).filter((c): c is ServiceCardCms => Boolean(c))
|
||||||
icon: Database,
|
|
||||||
title: 'Управление данными',
|
if (!title && !description && cards.length === 0) {
|
||||||
description: 'Развертывание и поддержка СУБД Postgres Pro',
|
return null
|
||||||
gradient: 'from-orange-50 to-amber-50',
|
}
|
||||||
iconColor: 'text-orange-600',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Server,
|
|
||||||
title: 'Серверная инфраструктура',
|
|
||||||
description: 'Поставка и настройка оборудования Kraftway',
|
|
||||||
gradient: 'from-pink-50 to-rose-50',
|
|
||||||
iconColor: 'text-pink-600',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Settings,
|
|
||||||
title: 'Системная интеграция',
|
|
||||||
description: 'Построение корпоративной ИТ-архитектуры на Astra Linux',
|
|
||||||
gradient: 'from-indigo-50 to-blue-50',
|
|
||||||
iconColor: 'text-indigo-600',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function Services() {
|
|
||||||
return (
|
return (
|
||||||
<section id="services" className="py-24 bg-white">
|
<section id="services" className="py-24 bg-white">
|
||||||
<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">
|
||||||
<div className="max-w-3xl mb-16">
|
<div className="max-w-3xl mb-16">
|
||||||
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">Услуги</p>
|
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">Услуги</p>
|
||||||
<h2 className="text-4xl lg:text-5xl text-gray-900 mb-6">Комплексные IT-решения для вашего бизнеса</h2>
|
{title ? <h2 className="text-4xl lg:text-5xl text-gray-900 mb-6">{title}</h2> : null}
|
||||||
<p className="text-xl text-gray-600">Полный цикл: от стратегии до технической поддержки</p>
|
{description ? <p className="text-xl text-gray-600">{description}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{services.map((service, index) => {
|
{cards.map((service, index) => {
|
||||||
const Icon = service.icon
|
const Icon = toComponent(service.icon?.value)
|
||||||
|
const gradient = toGradient(service.color?.value)
|
||||||
|
const iconColor = toTextColor(service.color?.value)
|
||||||
const floatClasses = [
|
const floatClasses = [
|
||||||
'animate-float',
|
'animate-float',
|
||||||
'animate-float-delay-1',
|
'animate-float-delay-1',
|
||||||
@@ -67,31 +54,44 @@ export function Services() {
|
|||||||
'animate-float-delay-5',
|
'animate-float-delay-5',
|
||||||
]
|
]
|
||||||
const floatClass = floatClasses[index % 6]
|
const floatClass = floatClasses[index % 6]
|
||||||
|
const slug = service.category?.slug
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={service.id}
|
||||||
className="group bg-white rounded-2xl border border-gray-100 hover:border-blue-200 hover:shadow-xl transition-all duration-300 overflow-hidden"
|
className="group bg-white rounded-2xl border border-gray-100 hover:border-blue-200 hover:shadow-xl transition-all duration-300 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative h-48 overflow-hidden bg-linear-to-br ${service.gradient} flex items-center justify-center`}
|
className={`relative h-48 overflow-hidden bg-linear-to-br ${gradient} flex items-center justify-center`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-20 h-20 bg-white rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-md ${floatClass}`}
|
className={`w-20 h-20 bg-white rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-md ${floatClass}`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-10 h-10 ${service.iconColor}`} />
|
<Icon className={`w-10 h-10 ${iconColor}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h3 className="text-xl text-gray-900 mb-3">{service.title}</h3>
|
{service.title ? <h3 className="text-xl text-gray-900 mb-3">{service.title}</h3> : null}
|
||||||
|
|
||||||
<p className="text-gray-600 mb-4 leading-relaxed">{service.description}</p>
|
{service.description ? (
|
||||||
|
<p className="text-gray-600 mb-4 leading-relaxed">{service.description}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<button className="text-blue-600 hover:text-blue-700 transition-colors inline-flex items-center gap-2">
|
{slug ? (
|
||||||
Подробнее
|
<a
|
||||||
<span className="transition-all">→</span>
|
href={`/${slug}`}
|
||||||
</button>
|
className="text-blue-600 hover:text-blue-700 transition-colors inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
|
Подробнее
|
||||||
|
<span className="transition-all">→</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-blue-600 inline-flex items-center gap-2">
|
||||||
|
Подробнее
|
||||||
|
<span className="transition-all">→</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,75 +1,83 @@
|
|||||||
import { Building2, ShoppingCart, Factory, Heart, GraduationCap, Landmark } from 'lucide-react'
|
import { toComponent } from '../lib/cms/icon'
|
||||||
|
import type { Enum_Componentsharedicon_Value } from '../graphql/graphql'
|
||||||
|
import type { Maybe } from '../lib/types'
|
||||||
|
|
||||||
const solutions = [
|
export interface SolutionCardCms {
|
||||||
{
|
id: string
|
||||||
icon: Building2,
|
title?: Maybe<string>
|
||||||
title: 'Корпоративный сектор',
|
description?: Maybe<string>
|
||||||
description: 'ERP, CRM, автоматизация документооборота',
|
footnote?: Maybe<string>
|
||||||
projects: '1200+ проектов',
|
icon?: Maybe<{ value?: Maybe<Enum_Componentsharedicon_Value> }>
|
||||||
},
|
category?: Maybe<{ slug: string }>
|
||||||
{
|
}
|
||||||
icon: ShoppingCart,
|
|
||||||
title: 'Ритейл и e-commerce',
|
export interface SolutionsCmsData {
|
||||||
description: 'Омниканальные платформы продаж',
|
title?: Maybe<string>
|
||||||
projects: '850+ проектов',
|
description?: Maybe<string>
|
||||||
},
|
cards?: Maybe<Array<Maybe<SolutionCardCms>>>
|
||||||
{
|
}
|
||||||
icon: Factory,
|
|
||||||
title: 'Производство',
|
interface SolutionsProps {
|
||||||
description: 'IoT, системы управления производством',
|
data?: Maybe<SolutionsCmsData>
|
||||||
projects: '650+ проектов',
|
}
|
||||||
},
|
|
||||||
{
|
export function Solutions({ data }: SolutionsProps) {
|
||||||
icon: Heart,
|
const title = data?.title ?? ''
|
||||||
title: 'Здравоохранение',
|
const description = data?.description ?? ''
|
||||||
description: 'Медицинские информационные системы',
|
const cards = (data?.cards ?? []).filter((c): c is SolutionCardCms => Boolean(c))
|
||||||
projects: '420+ проектов',
|
|
||||||
},
|
if (!title && !description && cards.length === 0) {
|
||||||
{
|
return null
|
||||||
icon: GraduationCap,
|
}
|
||||||
title: 'Образование',
|
|
||||||
description: 'Платформы дистанционного обучения',
|
|
||||||
projects: '380+ проектов',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Landmark,
|
|
||||||
title: 'Государственный сектор',
|
|
||||||
description: 'Цифровизация госуслуг',
|
|
||||||
projects: '500+ проектов',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function Solutions() {
|
|
||||||
return (
|
return (
|
||||||
<section id="solutions" className="py-24 bg-gray-50">
|
<section id="solutions" className="py-24 bg-gray-50">
|
||||||
<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">
|
||||||
<div className="max-w-3xl mb-16">
|
<div className="max-w-3xl mb-16">
|
||||||
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">Отраслевые решения</p>
|
<p className="text-sm uppercase tracking-wider text-gray-500 mb-4">Отраслевые решения</p>
|
||||||
<h2 className="text-4xl lg:text-5xl text-gray-900 mb-6">Решения под ключ для разных отраслей</h2>
|
{title ? <h2 className="text-4xl lg:text-5xl text-gray-900 mb-6">{title}</h2> : null}
|
||||||
<p className="text-xl text-gray-600">Глубокое понимание специфики бизнеса в каждой отрасли</p>
|
{description ? <p className="text-xl text-gray-600">{description}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{solutions.map((solution, index) => {
|
{cards.map(solution => {
|
||||||
const Icon = solution.icon
|
const Icon = toComponent(solution.icon?.value)
|
||||||
return (
|
const slug = solution.category?.slug
|
||||||
<div
|
|
||||||
key={index}
|
const inner = (
|
||||||
className="group p-8 bg-white rounded-2xl hover:shadow-xl border border-gray-100 hover:border-blue-100 transition-all duration-300 cursor-pointer"
|
<>
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4 mb-4">
|
<div className="flex items-start gap-4 mb-4">
|
||||||
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform shadow-sm">
|
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform shadow-sm">
|
||||||
<Icon className="w-6 h-6 text-white" />
|
<Icon className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg text-gray-900 mb-2">{solution.title}</h3>
|
{solution.title ? <h3 className="text-lg text-gray-900 mb-2">{solution.title}</h3> : null}
|
||||||
<p className="text-gray-600">{solution.description}</p>
|
{solution.description ? <p className="text-gray-600">{solution.description}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100">
|
{solution.footnote ? (
|
||||||
<span className="text-sm text-blue-600">{solution.projects}</span>
|
<div className="pt-4 border-t border-gray-100">
|
||||||
</div>
|
<span className="text-sm text-blue-600">{solution.footnote}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return slug ? (
|
||||||
|
<a
|
||||||
|
key={solution.id}
|
||||||
|
href={`/${slug}`}
|
||||||
|
className="group p-8 bg-white rounded-2xl hover:shadow-xl border border-gray-100 hover:border-blue-100 transition-all duration-300 cursor-pointer block"
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={solution.id}
|
||||||
|
className="group p-8 bg-white rounded-2xl hover:shadow-xl border border-gray-100 hover:border-blue-100 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import type { HomePageQueryQuery } from '../graphql/graphql'
|
||||||
import { Hero } from '../sections/Hero'
|
import { Hero } from '../sections/Hero'
|
||||||
import { Services } from '../sections/Services'
|
import { Services } from '../sections/Services'
|
||||||
import { Solutions } from '../sections/Solutions'
|
import { Solutions } from '../sections/Solutions'
|
||||||
@@ -7,11 +8,60 @@ import { About } from '../sections/About'
|
|||||||
import { Contact } from '../sections/Contact'
|
import { Contact } from '../sections/Contact'
|
||||||
import { ContactModal } from '../components/ContactModal'
|
import { ContactModal } from '../components/ContactModal'
|
||||||
import { scrollToHashTarget } from '../lib/scrollToHash'
|
import { scrollToHashTarget } from '../lib/scrollToHash'
|
||||||
|
import type { Maybe } from '../lib/types'
|
||||||
|
|
||||||
const HEADER_OFFSET_PX = 85
|
const HEADER_OFFSET_PX = 85
|
||||||
|
|
||||||
export function HomePage() {
|
type HomePageData = NonNullable<HomePageQueryQuery['homePage']>
|
||||||
|
type HomeSection = NonNullable<NonNullable<HomePageData['sections']>[number]>
|
||||||
|
|
||||||
|
type HeroSection = Extract<HomeSection, { __typename?: 'ComponentSectionsHomeHero' }>
|
||||||
|
type ServicesSection = Extract<HomeSection, { __typename?: 'ComponentSectionsServices' }>
|
||||||
|
type SolutionsSection = Extract<HomeSection, { __typename?: 'ComponentSectionsSolutions' }>
|
||||||
|
type CasesSection = Extract<HomeSection, { __typename?: 'ComponentSectionsCases' }>
|
||||||
|
type AboutSection = Extract<HomeSection, { __typename?: 'ComponentSectionsAbout' }>
|
||||||
|
|
||||||
|
function parseHomeSections(home: Maybe<HomePageData>) {
|
||||||
|
const sections = home?.sections ?? []
|
||||||
|
let hero: HeroSection | undefined
|
||||||
|
let services: ServicesSection | undefined
|
||||||
|
let solutions: SolutionsSection | undefined
|
||||||
|
let cases: CasesSection | undefined
|
||||||
|
let about: AboutSection | undefined
|
||||||
|
|
||||||
|
for (const s of sections) {
|
||||||
|
if (!s || s.__typename === 'Error') continue
|
||||||
|
switch (s.__typename) {
|
||||||
|
case 'ComponentSectionsHomeHero':
|
||||||
|
hero = s
|
||||||
|
break
|
||||||
|
case 'ComponentSectionsServices':
|
||||||
|
services = s
|
||||||
|
break
|
||||||
|
case 'ComponentSectionsSolutions':
|
||||||
|
solutions = s
|
||||||
|
break
|
||||||
|
case 'ComponentSectionsCases':
|
||||||
|
cases = s
|
||||||
|
break
|
||||||
|
case 'ComponentSectionsAbout':
|
||||||
|
about = s
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hero, services, solutions, cases, about }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HomePageProps {
|
||||||
|
homePage?: Maybe<HomePageData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HomePage({ homePage }: HomePageProps) {
|
||||||
const [contactOpen, setContactOpen] = useState(false)
|
const [contactOpen, setContactOpen] = useState(false)
|
||||||
|
const { hero, services, solutions, cases, about } = parseHomeSections(homePage ?? null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onHashChange = () => scrollToHashTarget(window.location.hash.slice(1))
|
const onHashChange = () => scrollToHashTarget(window.location.hash.slice(1))
|
||||||
@@ -23,14 +73,14 @@ export function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero onOpenContactModal={() => setContactOpen(true)} />
|
<Hero onOpenContactModal={() => setContactOpen(true)} data={hero} />
|
||||||
<Services />
|
<Services data={services} />
|
||||||
<Solutions />
|
<Solutions data={solutions} />
|
||||||
<div id="cases" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
<div id="cases" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||||
<Cases />
|
<Cases data={cases} />
|
||||||
</div>
|
</div>
|
||||||
<div id="about" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
<div id="about" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||||
<About />
|
<About data={about} />
|
||||||
</div>
|
</div>
|
||||||
<div id="contact" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
<div id="contact" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
|
||||||
<Contact />
|
<Contact />
|
||||||
|
|||||||
Reference in New Issue
Block a user