This commit is contained in:
2026-03-31 14:02:47 +05:00
parent 3be0625063
commit c4223aba7c
10 changed files with 162 additions and 37 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

+6 -10
View File
@@ -4,7 +4,7 @@ import { PhoneIcon } from './icons/PhoneIcon';
import { SendIcon } from './icons/SendIcon';
import { TelegramIcon } from './icons/TelegramIcon';
import { VKIcon } from './icons/VKIcon';
import { YoutubeIcon } from './icons/YoutubeIcon';
import { RutubeIcon } from './icons/RutubeIcon';
import { Link } from 'react-router-dom';
import { useModalStore } from '../stores/modalStore';
import { ModalWithForm } from './Layout/ModalWithForm';
@@ -38,9 +38,7 @@ export function Contacts() {
className="py-4"
width="full"
icon={<MailIcon />}
onClick={() => {
window.location.href = 'mailto:info@graff.tech';
}}
href="mailto:info@graff.tech"
>
<span className="btn-text opacity-80">Написать</span>
</Button>
@@ -49,9 +47,7 @@ export function Contacts() {
className="py-4"
width="full"
icon={<PhoneIcon />}
onClick={() => {
window.location.href = 'tel:88007700067';
}}
href="tel:88007700067"
>
<span className="btn-text opacity-80">Позвонить</span>
</Button>
@@ -60,10 +56,10 @@ export function Contacts() {
<h4 className="h4 font-medium">Социальные сети</h4>
<div className="gap-x-2 flex">
<Link
to={'https://www.youtube.com/@GRAFFtech'}
className="p-4 rounded-full opacity-80 border-[#3D425C] border hover:border-[#52587A] transition-all"
to={'https://rutube.ru/channel/25505040/'}
className="p-4 flex items-center justify-center rounded-full opacity-80 border-[#3D425C] border hover:border-[#52587A] transition-all"
>
<YoutubeIcon />
<RutubeIcon />
</Link>
<Link
to={'https://vk.com/graffinteractive?from=groups'}
+1 -1
View File
@@ -149,7 +149,7 @@ export function FeedbackForm({
mask={placeholder?.replace(/\d/g, '9') ?? ''}
maskChar={null}
value={phone}
placeholder={placeholder}
placeholder={'XXX XXX XX XX'}
onChange={e => setPhone(e.target.value.replace(/ /g, ''))}
className="transition-all bg-transparent rounded-none outline-none h4 placeholder:h4 placeholder:font-medium placeholder:select-none peer"
/>
+10 -3
View File
@@ -1,12 +1,19 @@
import { LogoIcon } from '../icons/LogoIcon';
import { Link, NavLink } from 'react-router-dom';
import { LogoWithoutText } from '../icons/LogoWithoutTextIcon';
export function Footer() {
return (
<footer className="sm:grid xl:grid-cols-[2fr_1fr_1fr] sm:grid-cols-2 sm:max-xl:grid-rows-2 bg-[#14161F]">
<div className="flex sm:items-center max-sm:flex-col sm:px-6 px-4 sm:py-9 py-4 border-t border-[#3D425C] gap-6 sm:max-xl:row-start-1 sm:max-xl:col-span-2">
<Link to={'/'}>
<LogoIcon />
<img
src="images/GRAFFinteractive.svg"
alt="GRAFFinteractive"
className="w-[118px] max-md:hidden"
/>
<div className="md:hidden">
<LogoWithoutText />
</div>
</Link>
<div className="flex flex-col gap-y-1">
<div className="flex gap-x-4 m-text">
@@ -20,7 +27,7 @@ export function Footer() {
<Link to="https://graff.tech">graff.tech</Link>
</div>
<p className="opacity-40 sm:font-medium m-text">
© 2024 GRAFF interactive. Все права защищены
© 2026 GRAFF interactive. Все права защищены
</p>
</div>
</div>
+4 -4
View File
@@ -1,5 +1,4 @@
import { Link } from 'react-router-dom';
import { LogoIcon } from '../icons/LogoIcon';
import { Button } from '../ui/Button';
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
import { useModalStore } from '../../stores/modalStore';
@@ -26,9 +25,10 @@ export function Header() {
return (
<header className="sticky top-0 lg:px-6 px-4 flex max-lg:justify-between items-center lg:h-16 h-12 border-b border-[#3D425C] bg-[#14161F] z-50">
<Link to={'/'} className="max-sm:hidden">
<ClassNameWrapper
element={<LogoIcon />}
className="h-8 lg:h-10 lg:w-[113px] w-[93px]"
<img
src="images/GRAFFinteractive.svg"
alt="GRAFFinteractive"
className="lg:w-[118px] sm:w-[80px] max-sm:hidden"
/>
</Link>
<Link to={'/'} className="sm:hidden">
+9 -3
View File
@@ -1,15 +1,16 @@
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
import { useModalStore } from '../../stores/modalStore';
import { Button } from '../ui/Button';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { ArrowRightIcon } from '../icons/ArrowRightIcon';
import { CloseIcon } from '../icons/CloseIcon';
import { FeedbackForm } from './FeedbackForm';
import { useOnClickOutside } from 'usehooks-ts';
export function ModalWithForm() {
const { setModal } = useModalStore();
const [sent, setSent] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
@@ -22,8 +23,13 @@ export function ModalWithForm() {
return () => document.removeEventListener('keydown', listener);
}, [setModal]);
useOnClickOutside(modalRef, () => setModal(false));
return (
<div className="fixed flex flex-col gap-4 top-0 right-0 h-full sm:w-[408px] w-full bg-[#14161F] overflow-y-auto sm:p-8 p-6">
<div
ref={modalRef}
className="fixed flex flex-col gap-4 top-0 right-0 h-full sm:w-[408px] w-full bg-[#14161F] overflow-y-auto sm:p-8 p-6"
>
{!sent ? (
<div className="space-y-8">
<div className="flex justify-between items-center">
+1 -1
View File
@@ -1,7 +1,7 @@
export function LogoIcon() {
return (
<svg
width={128}
width={138}
height={50}
viewBox="0 0 128 50"
fill="none"
+20
View File
@@ -0,0 +1,20 @@
export function RutubeIcon() {
return (
<svg
width="32"
height="32"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g opacity="0.8" transform="translate(2, 3)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.793 4.543H2.62V2.205h6.174c.36 0 .612.06.738.165.125.106.203.3.203.585v.84c0 .3-.078.494-.203.6-.126.104-.377.15-.738.15zM9.217.001H0V10h2.619V6.747h4.825L9.734 10h2.933l-2.525-3.268c.93-.132 1.349-.405 1.693-.855s.518-1.17.518-2.13v-.749c0-.57-.063-1.019-.173-1.364a2.26 2.26 0 0 0-.564-.914 2.6 2.6 0 0 0-.973-.555C10.268.06 9.797 0 9.218 0"
fill="#fff"
/>
</g>
</svg>
);
}
+37 -13
View File
@@ -8,6 +8,8 @@ interface ButtonProps {
disabled?: boolean;
className?: string;
onClick?: () => void;
/** Native link (mailto:, tel:, https://). More reliable than JS navigation on some mobile WebViews. */
href?: string;
}
export function Button({
@@ -18,25 +20,47 @@ export function Button({
disabled = false,
className,
onClick,
href,
}: ButtonProps) {
return (
<button
disabled={disabled}
onClick={onClick}
className={`group relative px-6 py-2 rounded-full min-w-fit ${
(color === 'primary'
? 'bg-gradient-to-r from-[#798FFF] to-[#D375FF]'
: '') ||
(color === 'secondary' ? 'outline outline-1 outline-[#3D425C]' : '')
} ${
icon ? 'pr-4' : ''
} flex gap-1 items-center overflow-hidden w-${width} ${className} justify-between`}
>
const sharedClassName = `group relative px-6 py-2 rounded-full min-w-fit ${
(color === 'primary'
? 'bg-gradient-to-r from-[#798FFF] to-[#D375FF]'
: '') ||
(color === 'secondary' ? 'outline outline-1 outline-[#3D425C]' : '')
} ${
icon ? 'pr-4' : ''
} flex gap-1 items-center overflow-hidden w-${width} ${className} justify-between`;
const inner = (
<>
<span className="group-hover:opacity-10 opacity-0 bg-black transition-opacity absolute top-0 left-0 w-full h-full"></span>
<span className={'relative font-medium' + (icon ? '' : ' m-auto')}>
{children}
</span>
<span className="relative">{icon}</span>
</>
);
if (href) {
return (
<a
href={disabled ? undefined : href}
className={sharedClassName + (disabled ? ' pointer-events-none opacity-50' : '')}
aria-disabled={disabled || undefined}
onClick={disabled ? (e) => e.preventDefault() : onClick}
>
{inner}
</a>
);
}
return (
<button
disabled={disabled}
onClick={onClick}
className={sharedClassName}
>
{inner}
</button>
);
}
+2 -2
View File
@@ -27,13 +27,13 @@ export const stands: IStand[] = [
},
{
img: 'images/stands/interactive_floor.jpg',
title: 'Interactive floor',
title: 'Интерактивный этаж',
annotation: 'Выставка «Россия ВДНХ»',
year: '2022',
},
{
img: 'images/stands/graff_estate_stream.jpg',
title: 'Graff.estate stream',
title: 'Модуль удаленной демонстрации недвижимости GRAFF.estate',
annotation: 'WOW FEST',
year: '2023',
},