UPD
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 611 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 594 KiB |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 780 B |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 763 B |
|
After Width: | Height: | Size: 836 B |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 809 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 949 B |
|
After Width: | Height: | Size: 250 B |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 738 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 949 B |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 840 B |
|
After Width: | Height: | Size: 644 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 861 B |
|
After Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 859 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.3 MiB |
|
After Width: | Height: | Size: 859 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 635 B |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 783 B |
|
After Width: | Height: | Size: 638 B |
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -852,7 +852,6 @@ type KeySegmentInfo {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
title: String
|
||||||
order: Int
|
order: Int
|
||||||
colSpan: KeySegmentInfoColSpanType
|
|
||||||
segment: KeySegment
|
segment: KeySegment
|
||||||
brandLink: String
|
brandLink: String
|
||||||
brandLogo: ImageFieldOutput
|
brandLogo: ImageFieldOutput
|
||||||
|
|
@ -860,11 +859,6 @@ type KeySegmentInfo {
|
||||||
itemsCount(where: KeySegmentInfoItemWhereInput! = {}): Int
|
itemsCount(where: KeySegmentInfoItemWhereInput! = {}): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KeySegmentInfoColSpanType {
|
|
||||||
ONE
|
|
||||||
TWO
|
|
||||||
}
|
|
||||||
|
|
||||||
input KeySegmentInfoWhereUniqueInput {
|
input KeySegmentInfoWhereUniqueInput {
|
||||||
id: ID
|
id: ID
|
||||||
}
|
}
|
||||||
|
|
@ -876,19 +870,11 @@ input KeySegmentInfoWhereInput {
|
||||||
id: IDFilter
|
id: IDFilter
|
||||||
title: StringFilter
|
title: StringFilter
|
||||||
order: IntNullableFilter
|
order: IntNullableFilter
|
||||||
colSpan: KeySegmentInfoColSpanTypeNullableFilter
|
|
||||||
segment: KeySegmentWhereInput
|
segment: KeySegmentWhereInput
|
||||||
brandLink: StringFilter
|
brandLink: StringFilter
|
||||||
items: KeySegmentInfoItemManyRelationFilter
|
items: KeySegmentInfoItemManyRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
input KeySegmentInfoColSpanTypeNullableFilter {
|
|
||||||
equals: KeySegmentInfoColSpanType
|
|
||||||
in: [KeySegmentInfoColSpanType!]
|
|
||||||
notIn: [KeySegmentInfoColSpanType!]
|
|
||||||
not: KeySegmentInfoColSpanTypeNullableFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
input KeySegmentInfoItemManyRelationFilter {
|
input KeySegmentInfoItemManyRelationFilter {
|
||||||
every: KeySegmentInfoItemWhereInput
|
every: KeySegmentInfoItemWhereInput
|
||||||
some: KeySegmentInfoItemWhereInput
|
some: KeySegmentInfoItemWhereInput
|
||||||
|
|
@ -899,14 +885,12 @@ input KeySegmentInfoOrderByInput {
|
||||||
id: OrderDirection
|
id: OrderDirection
|
||||||
title: OrderDirection
|
title: OrderDirection
|
||||||
order: OrderDirection
|
order: OrderDirection
|
||||||
colSpan: OrderDirection
|
|
||||||
brandLink: OrderDirection
|
brandLink: OrderDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
input KeySegmentInfoUpdateInput {
|
input KeySegmentInfoUpdateInput {
|
||||||
title: String
|
title: String
|
||||||
order: Int
|
order: Int
|
||||||
colSpan: KeySegmentInfoColSpanType
|
|
||||||
segment: KeySegmentRelateToOneForUpdateInput
|
segment: KeySegmentRelateToOneForUpdateInput
|
||||||
brandLink: String
|
brandLink: String
|
||||||
brandLogo: ImageFieldInput
|
brandLogo: ImageFieldInput
|
||||||
|
|
@ -934,7 +918,6 @@ input KeySegmentInfoUpdateArgs {
|
||||||
input KeySegmentInfoCreateInput {
|
input KeySegmentInfoCreateInput {
|
||||||
title: String
|
title: String
|
||||||
order: Int
|
order: Int
|
||||||
colSpan: KeySegmentInfoColSpanType
|
|
||||||
segment: KeySegmentRelateToOneForCreateInput
|
segment: KeySegmentRelateToOneForCreateInput
|
||||||
brandLink: String
|
brandLink: String
|
||||||
brandLogo: ImageFieldInput
|
brandLogo: ImageFieldInput
|
||||||
|
|
@ -955,10 +938,16 @@ type KeySegmentInfoItem {
|
||||||
id: ID!
|
id: ID!
|
||||||
text: String
|
text: String
|
||||||
infoLogo: ImageFieldOutput
|
infoLogo: ImageFieldOutput
|
||||||
|
colSpan: KeySegmentInfoItemColSpanType
|
||||||
order: Int
|
order: Int
|
||||||
info: KeySegmentInfo
|
info: KeySegmentInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum KeySegmentInfoItemColSpanType {
|
||||||
|
ONE
|
||||||
|
TWO
|
||||||
|
}
|
||||||
|
|
||||||
input KeySegmentInfoItemWhereUniqueInput {
|
input KeySegmentInfoItemWhereUniqueInput {
|
||||||
id: ID
|
id: ID
|
||||||
}
|
}
|
||||||
|
|
@ -969,19 +958,29 @@ input KeySegmentInfoItemWhereInput {
|
||||||
NOT: [KeySegmentInfoItemWhereInput!]
|
NOT: [KeySegmentInfoItemWhereInput!]
|
||||||
id: IDFilter
|
id: IDFilter
|
||||||
text: StringFilter
|
text: StringFilter
|
||||||
|
colSpan: KeySegmentInfoItemColSpanTypeNullableFilter
|
||||||
order: IntNullableFilter
|
order: IntNullableFilter
|
||||||
info: KeySegmentInfoWhereInput
|
info: KeySegmentInfoWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input KeySegmentInfoItemColSpanTypeNullableFilter {
|
||||||
|
equals: KeySegmentInfoItemColSpanType
|
||||||
|
in: [KeySegmentInfoItemColSpanType!]
|
||||||
|
notIn: [KeySegmentInfoItemColSpanType!]
|
||||||
|
not: KeySegmentInfoItemColSpanTypeNullableFilter
|
||||||
|
}
|
||||||
|
|
||||||
input KeySegmentInfoItemOrderByInput {
|
input KeySegmentInfoItemOrderByInput {
|
||||||
id: OrderDirection
|
id: OrderDirection
|
||||||
text: OrderDirection
|
text: OrderDirection
|
||||||
|
colSpan: OrderDirection
|
||||||
order: OrderDirection
|
order: OrderDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
input KeySegmentInfoItemUpdateInput {
|
input KeySegmentInfoItemUpdateInput {
|
||||||
text: String
|
text: String
|
||||||
infoLogo: ImageFieldInput
|
infoLogo: ImageFieldInput
|
||||||
|
colSpan: KeySegmentInfoItemColSpanType
|
||||||
order: Int
|
order: Int
|
||||||
info: KeySegmentInfoRelateToOneForUpdateInput
|
info: KeySegmentInfoRelateToOneForUpdateInput
|
||||||
}
|
}
|
||||||
|
|
@ -1000,6 +999,7 @@ input KeySegmentInfoItemUpdateArgs {
|
||||||
input KeySegmentInfoItemCreateInput {
|
input KeySegmentInfoItemCreateInput {
|
||||||
text: String
|
text: String
|
||||||
infoLogo: ImageFieldInput
|
infoLogo: ImageFieldInput
|
||||||
|
colSpan: KeySegmentInfoItemColSpanType
|
||||||
order: Int
|
order: Int
|
||||||
info: KeySegmentInfoRelateToOneForCreateInput
|
info: KeySegmentInfoRelateToOneForCreateInput
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,6 @@ model KeySegmentInfo {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
title String @default("")
|
title String @default("")
|
||||||
order Int? @default(0)
|
order Int? @default(0)
|
||||||
colSpan String? @default("ONE")
|
|
||||||
segment KeySegment? @relation("KeySegmentInfo_segment", fields: [segmentId], references: [id])
|
segment KeySegment? @relation("KeySegmentInfo_segment", fields: [segmentId], references: [id])
|
||||||
segmentId String? @map("segment")
|
segmentId String? @map("segment")
|
||||||
brandLink String @default("")
|
brandLink String @default("")
|
||||||
|
|
@ -169,6 +168,7 @@ model KeySegmentInfoItem {
|
||||||
infoLogo_width Int?
|
infoLogo_width Int?
|
||||||
infoLogo_height Int?
|
infoLogo_height Int?
|
||||||
infoLogo_extension String?
|
infoLogo_extension String?
|
||||||
|
colSpan String? @default("ONE")
|
||||||
order Int? @default(0)
|
order Int? @default(0)
|
||||||
info KeySegmentInfo? @relation("KeySegmentInfoItem_info", fields: [infoId], references: [id])
|
info KeySegmentInfo? @relation("KeySegmentInfoItem_info", fields: [infoId], references: [id])
|
||||||
infoId String? @map("info")
|
infoId String? @map("info")
|
||||||
|
|
|
||||||
|
|
@ -336,22 +336,51 @@ export const lists: Lists = {
|
||||||
many: true,
|
many: true,
|
||||||
ui: {
|
ui: {
|
||||||
displayMode: "cards",
|
displayMode: "cards",
|
||||||
cardFields: ["title", "colSpan", "order"],
|
cardFields: ["title", "order"],
|
||||||
inlineCreate: { fields: ["title", "colSpan", "order"] },
|
inlineCreate: { fields: ["title", "order", "brandLink", "brandLogo"] },
|
||||||
inlineEdit: { fields: ["title", "colSpan", "order"] },
|
inlineEdit: { fields: ["title", "order", "brandLink", "brandLogo"] },
|
||||||
linkToItem: true,
|
linkToItem: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
/* Info card (column block) */
|
/* Info card (column block) */
|
||||||
KeySegmentInfo: list({
|
KeySegmentInfo: list({
|
||||||
access: allowAll,
|
access: allowAll,
|
||||||
ui: { label: "Key Segment • Info Block", listView: { initialColumns: ["order", "title", "colSpan"] } },
|
ui: { label: "Key Segment • Info Block", listView: { initialColumns: ["order", "title"] } },
|
||||||
fields: {
|
fields: {
|
||||||
title: text({ validation: { isRequired: true } }),
|
title: text({ validation: { isRequired: true } }),
|
||||||
order: integer({ defaultValue: 0 }),
|
order: integer({ defaultValue: 0 }),
|
||||||
|
segment: relationship({ ref: "KeySegment.info" }),
|
||||||
|
|
||||||
|
// бренд принадлежит целому блоку
|
||||||
|
brandLink: text({ ui: { description: "Ссылка бренда (опц.)" } }),
|
||||||
|
brandLogo: image({ storage: 'local_images', ui: { description: "Лого бренда (опц.)" } }),
|
||||||
|
|
||||||
|
items: relationship({
|
||||||
|
ref: "KeySegmentInfoItem.info",
|
||||||
|
many: true,
|
||||||
|
ui: {
|
||||||
|
displayMode: "cards",
|
||||||
|
cardFields: ["text", "order", "infoLogo", "colSpan"],
|
||||||
|
inlineCreate: { fields: ["text", "infoLogo", "colSpan", "order"] },
|
||||||
|
inlineEdit: { fields: ["text", "infoLogo", "colSpan", "order"] },
|
||||||
|
linkToItem: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
/* Info list item (только текст + лого) */
|
||||||
|
KeySegmentInfoItem: list({
|
||||||
|
access: allowAll,
|
||||||
|
ui: { label: "Key Segment • Info Item", listView: { initialColumns: ["order", "text", "infoLogo", "colSpan"] } },
|
||||||
|
fields: {
|
||||||
|
text: text({ validation: { isRequired: true } }),
|
||||||
|
infoLogo: image({ storage: 'local_images', ui: { description: "Лого в зелёном квадратике" } }),
|
||||||
colSpan: select({
|
colSpan: select({
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
options: [
|
options: [
|
||||||
|
|
@ -361,38 +390,12 @@ export const lists: Lists = {
|
||||||
defaultValue: 'ONE',
|
defaultValue: 'ONE',
|
||||||
ui: { displayMode: 'segmented-control' },
|
ui: { displayMode: 'segmented-control' },
|
||||||
}),
|
}),
|
||||||
segment: relationship({ ref: "KeySegment.info" }),
|
|
||||||
|
|
||||||
// 🔽 теперь бренд принадлежит целому блоку
|
|
||||||
brandLink: text({ ui: { description: "Ссылка бренда (опц.)" } }),
|
|
||||||
brandLogo: image({ storage: 'local_images', ui: { description: "Лого бренда (опц.)" } }),
|
|
||||||
|
|
||||||
items: relationship({
|
|
||||||
ref: "KeySegmentInfoItem.info",
|
|
||||||
many: true,
|
|
||||||
ui: {
|
|
||||||
displayMode: "cards",
|
|
||||||
cardFields: ["text", "order", "infoLogo"],
|
|
||||||
inlineCreate: { fields: ["text", "infoLogo", "order"] },
|
|
||||||
inlineEdit: { fields: ["text", "infoLogo", "order"] },
|
|
||||||
linkToItem: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
/* Info list item (только текст + лого) */
|
|
||||||
KeySegmentInfoItem: list({
|
|
||||||
access: allowAll,
|
|
||||||
ui: { label: "Key Segment • Info Item", listView: { initialColumns: ["order", "text", "infoLogo"] } },
|
|
||||||
fields: {
|
|
||||||
text: text({ validation: { isRequired: true } }),
|
|
||||||
infoLogo: image({ storage: 'local_images', ui: { description: "Лого в зелёном квадратике" } }),
|
|
||||||
order: integer({ defaultValue: 0 }),
|
order: integer({ defaultValue: 0 }),
|
||||||
info: relationship({ ref: "KeySegmentInfo.items" }),
|
info: relationship({ ref: "KeySegmentInfo.items" }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
WhyChooseUsItem: list({
|
WhyChooseUsItem: list({
|
||||||
access: allowAll,
|
access: allowAll,
|
||||||
ui: {
|
ui: {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ const ContactFormSection: React.FC = () => {
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [sent, setSent] = React.useState(false);
|
const [sent, setSent] = React.useState(false);
|
||||||
|
|
||||||
|
// состояния согласий
|
||||||
|
const [agreePrivacy, setAgreePrivacy] = React.useState(true);
|
||||||
|
const [agreeComms, setAgreeComms] = React.useState(true);
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -53,10 +57,17 @@ const ContactFormSection: React.FC = () => {
|
||||||
const email = String(fd.get("email") || "").trim();
|
const email = String(fd.get("email") || "").trim();
|
||||||
const phone = String(fd.get("phone") || "").trim();
|
const phone = String(fd.get("phone") || "").trim();
|
||||||
|
|
||||||
|
// читаем согласия из формы (и дублируем защиту состояниями)
|
||||||
|
const consentPrivacy = fd.get("consentPrivacy") === "on";
|
||||||
|
const consentComms = fd.get("consentComms") === "on";
|
||||||
|
|
||||||
// простая валидация
|
// простая валидация
|
||||||
if (!name) return setError("Please enter your name.");
|
if (!name) return setError("Please enter your name.");
|
||||||
if (!country) return setError("Please choose a country from the list.");
|
if (!country) return setError("Please choose a country from the list.");
|
||||||
if (!email) return setError("Please enter your business email.");
|
if (!email) return setError("Please enter your business email.");
|
||||||
|
if (!consentPrivacy || !consentComms || !agreePrivacy || !agreeComms) {
|
||||||
|
return setError("Please accept both consents.");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
@ -67,7 +78,7 @@ const ContactFormSection: React.FC = () => {
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
countryIso: country,
|
countryCode: country,
|
||||||
company,
|
company,
|
||||||
position,
|
position,
|
||||||
email,
|
email,
|
||||||
|
|
@ -76,28 +87,11 @@ const ContactFormSection: React.FC = () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2) шлём письмо (если у тебя есть этот эндпоинт; ошибки не блокируют успех)
|
|
||||||
try {
|
|
||||||
await fetch("/api/send-mail", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
subject: "New contact request from landing",
|
|
||||||
text:
|
|
||||||
`Name: ${name}\n` +
|
|
||||||
`Country: ${country}\n` +
|
|
||||||
`Company: ${company}\n` +
|
|
||||||
`Position: ${position}\n` +
|
|
||||||
`Email: ${email}\n` +
|
|
||||||
`Phone: ${phone}\n`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// молча игнорим — форма уже сохранена в БД
|
|
||||||
}
|
|
||||||
|
|
||||||
setSent(true); // прячем форму, показываем «спасибо»
|
setSent(true); // прячем форму, показываем «спасибо»
|
||||||
form.reset(); // чистим поля, на всякий
|
form.reset(); // чистим поля, на всякий
|
||||||
|
// на всякий возвращаем чекбоксы в true (форма уже скрыта)
|
||||||
|
setAgreePrivacy(true);
|
||||||
|
setAgreeComms(true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || "Failed to submit. Please try again.");
|
setError(err?.message || "Failed to submit. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -170,7 +164,13 @@ const ContactFormSection: React.FC = () => {
|
||||||
{/* согласия */}
|
{/* согласия */}
|
||||||
<Consents>
|
<Consents>
|
||||||
<ConsentRow>
|
<ConsentRow>
|
||||||
<Checkbox type="checkbox" id="c1" defaultChecked />
|
<Checkbox
|
||||||
|
type="checkbox"
|
||||||
|
id="c1"
|
||||||
|
name="consentPrivacy"
|
||||||
|
checked={agreePrivacy}
|
||||||
|
onChange={(e) => setAgreePrivacy(e.target.checked)}
|
||||||
|
/>
|
||||||
<label htmlFor="c1">
|
<label htmlFor="c1">
|
||||||
I accept the{" "}
|
I accept the{" "}
|
||||||
<a href="/privacy" target="_blank" rel="noreferrer">
|
<a href="/privacy" target="_blank" rel="noreferrer">
|
||||||
|
|
@ -181,7 +181,13 @@ const ContactFormSection: React.FC = () => {
|
||||||
</ConsentRow>
|
</ConsentRow>
|
||||||
|
|
||||||
<ConsentRow>
|
<ConsentRow>
|
||||||
<Checkbox type="checkbox" id="c2" defaultChecked />
|
<Checkbox
|
||||||
|
type="checkbox"
|
||||||
|
id="c2"
|
||||||
|
name="consentComms"
|
||||||
|
checked={agreeComms}
|
||||||
|
onChange={(e) => setAgreeComms(e.target.checked)}
|
||||||
|
/>
|
||||||
<label htmlFor="c2">
|
<label htmlFor="c2">
|
||||||
I confirm my consent to receive information from the company via
|
I confirm my consent to receive information from the company via
|
||||||
the provided contact methods of Email and/or Telephone
|
the provided contact methods of Email and/or Telephone
|
||||||
|
|
@ -190,7 +196,7 @@ const ContactFormSection: React.FC = () => {
|
||||||
</Consents>
|
</Consents>
|
||||||
|
|
||||||
<Actions>
|
<Actions>
|
||||||
<SubmitButton type="submit" disabled={busy}>
|
<SubmitButton type="submit" disabled={busy || !agreePrivacy || !agreeComms}>
|
||||||
{busy ? "Sending…" : "Submit a request"}
|
{busy ? "Sending…" : "Submit a request"}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</Actions>
|
</Actions>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ const GEO_URL = "/data/countries-110m.json";
|
||||||
const CARD_W = 290;
|
const CARD_W = 290;
|
||||||
const CARD_H = 160;
|
const CARD_H = 160;
|
||||||
|
|
||||||
|
const MIN_ZOOM = 0.8;
|
||||||
|
const MAX_ZOOM = 8;
|
||||||
|
const INITIAL_ZOOM = 4;
|
||||||
|
const ZOOM_STEP = 1.25;
|
||||||
|
|
||||||
|
const clamp = (n: number, min: number, max: number) =>
|
||||||
|
Math.min(max, Math.max(min, n));
|
||||||
|
|
||||||
|
|
||||||
/* =================== CMS types/selection =================== */
|
/* =================== CMS types/selection =================== */
|
||||||
type CmsItem = { id: string; text: string; order: number };
|
type CmsItem = { id: string; text: string; order: number };
|
||||||
type CmsCountry = {
|
type CmsCountry = {
|
||||||
|
|
@ -110,6 +119,33 @@ function mapCountries(section?: CmsSection | null): CountryCard[] {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateScaleKeepCenter(newScale: number) {
|
||||||
|
const g = document.querySelector<SVGGElement>("g.rsm-zoomable-group");
|
||||||
|
const svg = g?.closest("svg") as SVGSVGElement | null;
|
||||||
|
if (!g || !svg) return;
|
||||||
|
|
||||||
|
const transform = g.getAttribute("transform") || "";
|
||||||
|
const matchT = transform.match(/translate\((-?\d+\.?\d*)[, ]+(-?\d+\.?\d*)\)/);
|
||||||
|
const matchS = transform.match(/scale\((-?\d+\.?\d*)\)/);
|
||||||
|
|
||||||
|
let tx = matchT ? parseFloat(matchT[1]) : 0;
|
||||||
|
let ty = matchT ? parseFloat(matchT[2]) : 0;
|
||||||
|
const currentScale = matchS ? parseFloat(matchS[1]) : 1;
|
||||||
|
|
||||||
|
// центр SVG
|
||||||
|
const cx = svg.clientWidth / 2;
|
||||||
|
const cy = svg.clientHeight / 2;
|
||||||
|
|
||||||
|
// пересчёт translate
|
||||||
|
const ratio = newScale / currentScale;
|
||||||
|
tx = cx - (cx - tx) * ratio;
|
||||||
|
ty = cy - (cy - ty) * ratio;
|
||||||
|
|
||||||
|
g.setAttribute("transform", `translate(${tx} ${ty}) scale(${newScale})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* =================== component =================== */
|
/* =================== component =================== */
|
||||||
export default function CountriesMap() {
|
export default function CountriesMap() {
|
||||||
const wrapRef = useRef<HTMLDivElement>(null);
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -142,7 +178,20 @@ export default function CountriesMap() {
|
||||||
const initialCenter: [number, number] = mobile ? [65, 38] : [52, 38];
|
const initialCenter: [number, number] = mobile ? [65, 38] : [52, 38];
|
||||||
|
|
||||||
const [center, setCenter] = useState<[number, number]>(initialCenter);
|
const [center, setCenter] = useState<[number, number]>(initialCenter);
|
||||||
const [zoom, setZoom] = useState(4);
|
const [zoom, setZoom] = useState(INITIAL_ZOOM);
|
||||||
|
const [zoomScale, setZoomScale] = useState(INITIAL_ZOOM);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateScaleKeepCenter(zoomScale);
|
||||||
|
},[zoomScale]);
|
||||||
|
|
||||||
|
const zoomIn = () => setZoomScale(z => clamp(z * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM));
|
||||||
|
const zoomOut = () => setZoomScale(z => clamp(z / ZOOM_STEP, MIN_ZOOM, MAX_ZOOM));
|
||||||
|
const resetView = () => {
|
||||||
|
setCenter(initialCenter);
|
||||||
|
setZoom(INITIAL_ZOOM);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap ref={wrapRef}>
|
<Wrap ref={wrapRef}>
|
||||||
|
|
@ -161,6 +210,7 @@ export default function CountriesMap() {
|
||||||
minZoom={0.8}
|
minZoom={0.8}
|
||||||
maxZoom={8}
|
maxZoom={8}
|
||||||
translateExtent={[[-1000, -500], [1000, 900]]}
|
translateExtent={[[-1000, -500], [1000, 900]]}
|
||||||
|
filterZoomEvent={(e) => e.type !== "wheel" && e.type !== "dblclick"}
|
||||||
onMoveEnd={() => {
|
onMoveEnd={() => {
|
||||||
setCenter(initialCenter);
|
setCenter(initialCenter);
|
||||||
setZoom(zoom);
|
setZoom(zoom);
|
||||||
|
|
@ -306,6 +356,12 @@ export default function CountriesMap() {
|
||||||
})}
|
})}
|
||||||
</ZoomableGroup>
|
</ZoomableGroup>
|
||||||
</ComposableMap>
|
</ComposableMap>
|
||||||
|
<ZoomControls>
|
||||||
|
|
||||||
|
<ZoomBtn onClick={zoomIn} aria-label="Zoom in">+</ZoomBtn>
|
||||||
|
<ZoomBtn onClick={zoomOut} aria-label="Zoom out">–</ZoomBtn>
|
||||||
|
<ZoomBtn onClick={resetView} aria-label="Reset">⟳</ZoomBtn>
|
||||||
|
</ZoomControls>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -318,3 +374,31 @@ const Wrap = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ZoomControls = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ZoomBtn = styled.button`
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #14935f;
|
||||||
|
box-shadow: 0px 8px 14.13px 0px #2764698f;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
&:active { transform: translateY(1px); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,15 @@ const FooterSection: React.FC = () => {
|
||||||
<Inner>
|
<Inner>
|
||||||
<Top>
|
<Top>
|
||||||
<Left>
|
<Left>
|
||||||
<LogoRow>
|
<LogoRow >
|
||||||
<Logo src={logoSrc} alt="Logo" />
|
<Logo
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = "/#freedom";
|
||||||
|
}}
|
||||||
|
src={logoSrc}
|
||||||
|
alt="Logo"
|
||||||
|
/>
|
||||||
|
|
||||||
</LogoRow>
|
</LogoRow>
|
||||||
|
|
||||||
<Nav>
|
<Nav>
|
||||||
|
|
@ -143,6 +150,9 @@ const Wrap = styled.footer`
|
||||||
background: #1d2023;
|
background: #1d2023;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
padding: 64px 40px;
|
padding: 64px 40px;
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
padding: 32px 40px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Inner = styled.div`
|
const Inner = styled.div`
|
||||||
|
|
@ -155,7 +165,6 @@ const Top = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Left = styled.div`
|
const Left = styled.div`
|
||||||
max-width: 610px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LogoRow = styled.div`
|
const LogoRow = styled.div`
|
||||||
|
|
|
||||||
|
|
@ -89,14 +89,34 @@ const Submenu = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Burger = styled.button`
|
const Burger = styled.button<{ open: boolean }>`
|
||||||
display: none;
|
display: none;
|
||||||
@media (max-width: 768px) { display:flex; }
|
@media (max-width: 768px) { display:flex; }
|
||||||
width: 24px; height: 18px; flex-direction: column; justify-content: space-between; cursor:pointer; margin-left:12px;
|
width: 24px; height: 18px;
|
||||||
|
flex-direction: column; justify-content: space-between;
|
||||||
|
cursor:pointer; margin-left:12px;
|
||||||
background: transparent; border: 0;
|
background: transparent; border: 0;
|
||||||
span { display:block; height:3px; border-radius:2px; background:#1d2023; }
|
|
||||||
|
span {
|
||||||
|
display:block;
|
||||||
|
height:3px; width:100%;
|
||||||
|
border-radius:3px;
|
||||||
|
background:#1d2023;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(1) {
|
||||||
|
transform: ${({ open }) => open ? "rotate(45deg) translate(5px, 5px)" : "none"};
|
||||||
|
}
|
||||||
|
span:nth-child(2) {
|
||||||
|
opacity: ${({ open }) => open ? 0 : 1};
|
||||||
|
}
|
||||||
|
span:nth-child(3) {
|
||||||
|
transform: ${({ open }) => open ? "rotate(-45deg) translate(5px, -5px)" : "none"};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const MobileMenu = styled.div<{ open: boolean }>`
|
const MobileMenu = styled.div<{ open: boolean }>`
|
||||||
position: absolute; top: 48px; right: 0; width: 100%;
|
position: absolute; top: 48px; right: 0; width: 100%;
|
||||||
background:#fff; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
background:#fff; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
|
@ -165,7 +185,7 @@ function Header() {
|
||||||
<HeaderPlaceholder />
|
<HeaderPlaceholder />
|
||||||
<HeaderBlockOver>
|
<HeaderBlockOver>
|
||||||
<HeaderBlock>
|
<HeaderBlock>
|
||||||
<img src={logoSrc} style={{ marginRight: "auto", height: "28px" }} alt={system?.siteTitle || "Logo"} />
|
<img onClick={() => handleGo("/#freedom")} src={logoSrc} style={{ marginRight: "auto", height: "28px" }} alt={system?.siteTitle || "Logo"} />
|
||||||
|
|
||||||
{/* Desktop menu */}
|
{/* Desktop menu */}
|
||||||
<Menu>
|
<Menu>
|
||||||
|
|
@ -199,7 +219,7 @@ function Header() {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Burger */}
|
{/* Burger */}
|
||||||
<Burger onClick={() => setOpen(v => !v)} aria-label="Menu">
|
<Burger open={open} onClick={() => setOpen(v => !v)} aria-label="Menu">
|
||||||
<span /><span /><span />
|
<span /><span /><span />
|
||||||
</Burger>
|
</Burger>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import {
|
import {
|
||||||
s_Text40_700 as Text40_700,
|
s_Text40_700 as Text40_700,
|
||||||
s_Text24_700 as Text24_700,
|
Text24_700 as Text24_700,
|
||||||
s_f_Text18_400 as Text18_400,
|
s_f_Text18_400 as Text18_400,
|
||||||
} from "../Typography";
|
} from "../Typography";
|
||||||
import { useSingleton } from "cms/factory";
|
import { useSingleton } from "cms/factory";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import styled, { css } from "styled-components";
|
||||||
import {
|
import {
|
||||||
s_Text40_700 as Text40_700,
|
s_Text40_700 as Text40_700,
|
||||||
s_Text24_700 as Text24_700,
|
s_Text24_700 as Text24_700,
|
||||||
s_f_Text18_400 as Text18_400,
|
Text18_400 as Text18_400_dom,
|
||||||
|
Text24_700 as Text24_700_dom,
|
||||||
} from "../Typography";
|
} from "../Typography";
|
||||||
import { useSingleton } from "cms/factory";
|
import { useSingleton } from "cms/factory";
|
||||||
import fallbackLogo from "imgs/logo.svg";
|
import fallbackLogo from "imgs/logo.svg";
|
||||||
|
|
@ -15,6 +16,7 @@ type CmsItem = {
|
||||||
text: string;
|
text: string;
|
||||||
order: number;
|
order: number;
|
||||||
infoLogo?: { url?: string | null } | null;
|
infoLogo?: { url?: string | null } | null;
|
||||||
|
colSpan?: "ONE" | "TWO" | "1" | "2" | 1 | 2 | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CmsInfo = {
|
type CmsInfo = {
|
||||||
|
|
@ -50,17 +52,17 @@ const SELECTION = `
|
||||||
id key name order
|
id key name order
|
||||||
logo { url }
|
logo { url }
|
||||||
info(orderBy: { order: asc }) {
|
info(orderBy: { order: asc }) {
|
||||||
id title order colSpan
|
id title order
|
||||||
brandLink
|
brandLink
|
||||||
brandLogo { url }
|
brandLogo { url }
|
||||||
items(orderBy: { order: asc }) {
|
items(orderBy: { order: asc }) {
|
||||||
id text order infoLogo { url }
|
id text order colSpan infoLogo { url }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const toSpan = (v: CmsInfo["colSpan"]) =>
|
const toSpan = (v: CmsItem["colSpan"]) =>
|
||||||
v === "TWO" || v === "2" || v === 2 ? 2 : 1;
|
v === "TWO" || v === "2" || v === 2 ? 2 : 1;
|
||||||
|
|
||||||
/* ===== View-model mapping ===== */
|
/* ===== View-model mapping ===== */
|
||||||
|
|
@ -73,13 +75,13 @@ function mapSegments(s?: CmsSection | null) {
|
||||||
info: (seg.info ?? []).map((block) => ({
|
info: (seg.info ?? []).map((block) => ({
|
||||||
id: block.id,
|
id: block.id,
|
||||||
title: block.title,
|
title: block.title,
|
||||||
span: toSpan(block.colSpan),
|
|
||||||
brandLink: block.brandLink ?? undefined,
|
brandLink: block.brandLink ?? undefined,
|
||||||
brandLogo: block.brandLogo?.url ?? undefined,
|
brandLogo: block.brandLogo?.url ?? undefined,
|
||||||
items: (block.items ?? []).map((it) => ({
|
items: (block.items ?? []).map((it) => ({
|
||||||
id: it.id,
|
id: it.id,
|
||||||
text: it.text,
|
text: it.text,
|
||||||
infoLogo: it.infoLogo?.url ?? undefined,
|
infoLogo: it.infoLogo?.url ?? undefined,
|
||||||
|
span: toSpan(it.colSpan), // ← ТУТ ВАЖНО
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
@ -164,6 +166,7 @@ const LogoCard = styled.div<{ $active: boolean }>`
|
||||||
transition: flex-basis 220ms ease, height 220ms ease, box-shadow 220ms ease,
|
transition: flex-basis 220ms ease, height 220ms ease, box-shadow 220ms ease,
|
||||||
transform 220ms ease;
|
transform 220ms ease;
|
||||||
${({ $active }) => ($active ? css`box-shadow: ${CARD_SHADOW};` : css`box-shadow: none;`)}
|
${({ $active }) => ($active ? css`box-shadow: ${CARD_SHADOW};` : css`box-shadow: none;`)}
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
flex: 0 0
|
flex: 0 0
|
||||||
|
|
@ -189,12 +192,11 @@ const LogoImg = styled.div<{ $active: boolean }>`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InfoGrid = styled.div`
|
const InfoGrid = styled.div`
|
||||||
display: grid;
|
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
grid-template-columns: 1fr 1fr;
|
column-count: 2; /* ← две колонки */
|
||||||
gap: 24px 32px;
|
column-gap: 32px; /* ← горизонтальный зазор между колонками */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
grid-template-columns: 1fr;
|
column-count: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -204,32 +206,46 @@ const InfoGridSizer = styled.div<{ $minh: number }>`
|
||||||
min-height: ${({ $minh }) => ($minh ? `${$minh}px` : "0px")};
|
min-height: ${({ $minh }) => ($minh ? `${$minh}px` : "0px")};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InfoCard = styled.div<{ $span: number }>`
|
// карточка должна быть «неделимой» и вести себя как блочный элемент в колонке
|
||||||
|
const InfoCard = styled.div`
|
||||||
|
display: inline-block; /* ← важно для columns */
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 24px; /* ← вертикальный зазор между карточками */
|
||||||
|
break-inside: avoid; /* стандарт */
|
||||||
|
-webkit-column-break-inside: avoid; /* сафари/старые вебкиты */
|
||||||
|
-moz-column-break-inside: avoid;
|
||||||
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: ${CARD_SHADOW};
|
box-shadow: ${CARD_SHADOW};
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
grid-column: span ${({ $span }) => Math.max(1, Math.min(2, $span))};
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
grid-column: span 1;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InfoTitleRow = styled.div`
|
const InfoTitleRow = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 12px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Bullets = styled.ul`
|
const Bullets = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
grid-template-columns: 1fr 1fr; /* 2 колонки */
|
||||||
|
gap: 16px 24px;
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: 1fr; /* на мобилке одна колонка */
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// li с поддержкой span
|
||||||
|
const BulletItem = styled.li<{ $span: number }>`
|
||||||
|
grid-column: span ${({ $span }) => Math.min(2, Math.max(1, $span))};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CheckDot = styled.div`
|
const CheckDot = styled.div`
|
||||||
|
|
@ -399,7 +415,7 @@ export default function KeyBusinessSegments() {
|
||||||
title={seg.name}
|
title={seg.name}
|
||||||
>
|
>
|
||||||
<LogoImg $active={isActive}>
|
<LogoImg $active={isActive}>
|
||||||
<img width="100%" height="100%" src={seg.logoSrc} alt={seg.name} />
|
<img height="100%" style={{maxHeight: '100%'}} src={seg.logoSrc} alt={seg.name} />
|
||||||
</LogoImg>
|
</LogoImg>
|
||||||
</LogoCard>
|
</LogoCard>
|
||||||
);
|
);
|
||||||
|
|
@ -412,9 +428,9 @@ export default function KeyBusinessSegments() {
|
||||||
{activeSegment ? (
|
{activeSegment ? (
|
||||||
<InfoGrid>
|
<InfoGrid>
|
||||||
{activeSegment.info.map((block) => (
|
{activeSegment.info.map((block) => (
|
||||||
<InfoCard key={block.id} $span={block.span}>
|
<InfoCard key={block.id}>
|
||||||
<InfoTitleRow>
|
<InfoTitleRow>
|
||||||
<Text24_700>{block.title}</Text24_700>
|
<Text24_700_dom>{block.title}</Text24_700_dom>
|
||||||
{block.brandLogo ? (
|
{block.brandLogo ? (
|
||||||
<a
|
<a
|
||||||
href={block.brandLink || "#"}
|
href={block.brandLink || "#"}
|
||||||
|
|
@ -422,29 +438,30 @@ export default function KeyBusinessSegments() {
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ display: "inline-flex", alignItems: "center" }}
|
style={{ display: "inline-flex", alignItems: "center" }}
|
||||||
>
|
>
|
||||||
<img src={block.brandLogo} alt="brand" style={{ height: 24 }} />
|
<img src={block.brandLogo} alt="brand" style={{ height: 32 }} />
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
</InfoTitleRow>
|
</InfoTitleRow>
|
||||||
|
{block.items?.length > 0 && (
|
||||||
<Bullets>
|
<Bullets>
|
||||||
{block.items.map((item) => (
|
{block.items.map((item) => (
|
||||||
<li key={item.id}>
|
<BulletItem key={item.id} $span={item.span}>
|
||||||
<ItemRow>
|
<ItemRow>
|
||||||
<CheckDot
|
<CheckDot
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: item.infoLogo
|
backgroundImage: item.infoLogo ? `url(${item.infoLogo})` : undefined,
|
||||||
? `url(${item.infoLogo})`
|
}}
|
||||||
: undefined,
|
/>
|
||||||
}}
|
<Text18_400_dom
|
||||||
/>
|
style={{ textAlign: "left", marginRight: item.infoLogo ? 10 : 0 }}
|
||||||
<Text18_400 style={{ marginRight: item.infoLogo ? 10 : 0 }}>
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</Text18_400>
|
</Text18_400_dom>
|
||||||
</ItemRow>
|
</ItemRow>
|
||||||
</li>
|
</BulletItem>
|
||||||
))}
|
))}
|
||||||
</Bullets>
|
</Bullets>
|
||||||
|
)}
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
))}
|
))}
|
||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
@ -503,29 +520,33 @@ function HiddenSizer({
|
||||||
{segments.map((seg, i) => (
|
{segments.map((seg, i) => (
|
||||||
<InfoGrid key={`sizer-${seg.id}`} ref={(el) => { refs.current[i] = el; }}>
|
<InfoGrid key={`sizer-${seg.id}`} ref={(el) => { refs.current[i] = el; }}>
|
||||||
{seg.info.map((block) => (
|
{seg.info.map((block) => (
|
||||||
<InfoCard key={block.id} $span={block.span}>
|
<InfoCard key={block.id}>
|
||||||
<InfoTitleRow>
|
<InfoTitleRow>
|
||||||
<Text24_700>{block.title}</Text24_700>
|
<Text24_700_dom>{block.title}</Text24_700_dom>
|
||||||
{block.brandLogo ? (
|
{block.brandLogo ? (
|
||||||
<img src={block.brandLogo} alt="" style={{ height: 24 }} />
|
<img src={block.brandLogo} alt="" style={{ height: 32 }} />
|
||||||
) : null}
|
) : null}
|
||||||
</InfoTitleRow>
|
</InfoTitleRow>
|
||||||
<Bullets>
|
{block.items?.length > 0 && (
|
||||||
{block.items.map((item) => (
|
<Bullets>
|
||||||
<li key={item.id}>
|
{block.items.map((item) => (
|
||||||
<ItemRow>
|
<BulletItem key={item.id} $span={item.span}>
|
||||||
<CheckDot
|
<ItemRow>
|
||||||
style={{
|
<CheckDot
|
||||||
backgroundImage: item.infoLogo
|
style={{
|
||||||
? `url(${item.infoLogo})`
|
backgroundImage: item.infoLogo ? `url(${item.infoLogo})` : undefined,
|
||||||
: undefined,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Text18_400_dom
|
||||||
<Text18_400>{item.text}</Text18_400>
|
style={{ textAlign: "left", marginRight: item.infoLogo ? 10 : 0 }}
|
||||||
</ItemRow>
|
>
|
||||||
</li>
|
{item.text}
|
||||||
))}
|
</Text18_400_dom>
|
||||||
</Bullets>
|
</ItemRow>
|
||||||
|
</BulletItem>
|
||||||
|
))}
|
||||||
|
</Bullets>
|
||||||
|
)}
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
))}
|
))}
|
||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,23 @@ const MobileButton = styled(Button)`
|
||||||
left: 16px;
|
left: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
|
|
||||||
|
opacity: 0; /* изначально невидим */
|
||||||
|
animation: fadeIn 1s ease forwards;
|
||||||
|
animation-delay: 1s; /* первая секунда — скрыт */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
function ScreenFirst() {
|
function ScreenFirst() {
|
||||||
const { data: hero, loading, error } = useSingleton<Hero>(
|
const { data: hero, loading, error } = useSingleton<Hero>(
|
||||||
"HeroSection",
|
"HeroSection",
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ function ScreenSecond() {
|
||||||
const logoSrc = section?.logo?.url || fallbackLogo;
|
const logoSrc = section?.logo?.url || fallbackLogo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenSecondBlock>
|
<ScreenSecondBlock id="holding">
|
||||||
<ContentCard>
|
<ContentCard>
|
||||||
<AsyncReveal loading={loading} error={error} stagger={0.08} from={12}>
|
<AsyncReveal loading={loading} error={error} stagger={0.08} from={12}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ const StatCardRow = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StatCardBox = styled.div`
|
const StatCardBox = styled.div`
|
||||||
background: #f9fafb;
|
background: #F5F8FB;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 33px 32px;
|
padding: 33px 32px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -190,7 +190,7 @@ function ScreenThree() {
|
||||||
const indexes: IndexItem[] = s3?.indexes ?? [];
|
const indexes: IndexItem[] = s3?.indexes ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenSecondBlock>
|
<ScreenSecondBlock id="business_areas">
|
||||||
<ContentCard>
|
<ContentCard>
|
||||||
<AsyncReveal loading={loading} error={error} stagger={0.08} from={12}>
|
<AsyncReveal loading={loading} error={error} stagger={0.08} from={12}>
|
||||||
{/* Заголовок + описание */}
|
{/* Заголовок + описание */}
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export default function SolutionsSection({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section id="solutions">
|
||||||
<Header>
|
<Header>
|
||||||
<Text40_700>{section?.title || "Solutions"}</Text40_700>
|
<Text40_700>{section?.title || "Solutions"}</Text40_700>
|
||||||
<Controls>
|
<Controls>
|
||||||
|
|
@ -270,7 +270,7 @@ export default function SolutionsSection({
|
||||||
<CardLink href={it.href}>
|
<CardLink href={it.href}>
|
||||||
<Text18_400
|
<Text18_400
|
||||||
as="span"
|
as="span"
|
||||||
style={{ textDecoration: "underline" }}
|
style={{ textDecoration: "underline", color:'#fff' }}
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</Text18_400>
|
</Text18_400>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ export const Text14_500 = styled.p`
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
@ -80,7 +79,6 @@ export const Text52_700 = styled.p`
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 38px;
|
line-height: 38px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -112,7 +110,6 @@ export const Text18_500 = styled.p`
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
@ -139,7 +136,6 @@ export const Text24_700 = styled.p`
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -176,9 +172,8 @@ export const s_f_Text18_400 = styled.p`
|
||||||
font-style: Regular;
|
font-style: Regular;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 24px;
|
line-height: 20px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -209,7 +204,6 @@ export const s_Text24_700 = styled.p`
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
@ -245,7 +239,6 @@ export const s_Text18_400 = styled.p`
|
||||||
leading-trim: NONE;
|
leading-trim: NONE;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
letter-spacing: 0%;
|
letter-spacing: 0%;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ const WhyChooseUs: React.FC<WhyChooseUsProps> = ({
|
||||||
(data?.items?.length ? data.items.map((i) => i.text) : items) ?? [];
|
(data?.items?.length ? data.items.map((i) => i.text) : items) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section id="why_choose_us">
|
||||||
<AsyncReveal loading={loading} error={error}>
|
<AsyncReveal loading={loading} error={error}>
|
||||||
<TitleRow>
|
<TitleRow>
|
||||||
<Text40_700>{uiTitle}</Text40_700>
|
<Text40_700>{uiTitle}</Text40_700>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ declare module "react-simple-maps" {
|
||||||
onMoveStart?: (pos: { coordinates: [number, number]; zoom: number }) => void;
|
onMoveStart?: (pos: { coordinates: [number, number]; zoom: number }) => void;
|
||||||
onMove?: (pos: { coordinates: [number, number]; zoom: number }) => void; // ← добавили
|
onMove?: (pos: { coordinates: [number, number]; zoom: number }) => void; // ← добавили
|
||||||
onMoveEnd?: (pos: { coordinates: [number, number]; zoom: number }) => void;
|
onMoveEnd?: (pos: { coordinates: [number, number]; zoom: number }) => void;
|
||||||
|
filterZoomEvent?: (e: any) => boolean;
|
||||||
}
|
}
|
||||||
export const ZoomableGroup: React.FC<ZoomableGroupProps>;
|
export const ZoomableGroup: React.FC<ZoomableGroupProps>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||