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 { SendIcon } from './icons/SendIcon';
import { TelegramIcon } from './icons/TelegramIcon'; import { TelegramIcon } from './icons/TelegramIcon';
import { VKIcon } from './icons/VKIcon'; import { VKIcon } from './icons/VKIcon';
import { YoutubeIcon } from './icons/YoutubeIcon'; import { RutubeIcon } from './icons/RutubeIcon';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useModalStore } from '../stores/modalStore'; import { useModalStore } from '../stores/modalStore';
import { ModalWithForm } from './Layout/ModalWithForm'; import { ModalWithForm } from './Layout/ModalWithForm';
@@ -38,9 +38,7 @@ export function Contacts() {
className="py-4" className="py-4"
width="full" width="full"
icon={<MailIcon />} icon={<MailIcon />}
onClick={() => { href="mailto:info@graff.tech"
window.location.href = 'mailto:info@graff.tech';
}}
> >
<span className="btn-text opacity-80">Написать</span> <span className="btn-text opacity-80">Написать</span>
</Button> </Button>
@@ -49,9 +47,7 @@ export function Contacts() {
className="py-4" className="py-4"
width="full" width="full"
icon={<PhoneIcon />} icon={<PhoneIcon />}
onClick={() => { href="tel:88007700067"
window.location.href = 'tel:88007700067';
}}
> >
<span className="btn-text opacity-80">Позвонить</span> <span className="btn-text opacity-80">Позвонить</span>
</Button> </Button>
@@ -60,10 +56,10 @@ export function Contacts() {
<h4 className="h4 font-medium">Социальные сети</h4> <h4 className="h4 font-medium">Социальные сети</h4>
<div className="gap-x-2 flex"> <div className="gap-x-2 flex">
<Link <Link
to={'https://www.youtube.com/@GRAFFtech'} to={'https://rutube.ru/channel/25505040/'}
className="p-4 rounded-full opacity-80 border-[#3D425C] border hover:border-[#52587A] transition-all" className="p-4 flex items-center justify-center rounded-full opacity-80 border-[#3D425C] border hover:border-[#52587A] transition-all"
> >
<YoutubeIcon /> <RutubeIcon />
</Link> </Link>
<Link <Link
to={'https://vk.com/graffinteractive?from=groups'} to={'https://vk.com/graffinteractive?from=groups'}
+1 -1
View File
@@ -149,7 +149,7 @@ export function FeedbackForm({
mask={placeholder?.replace(/\d/g, '9') ?? ''} mask={placeholder?.replace(/\d/g, '9') ?? ''}
maskChar={null} maskChar={null}
value={phone} value={phone}
placeholder={placeholder} placeholder={'XXX XXX XX XX'}
onChange={e => setPhone(e.target.value.replace(/ /g, ''))} 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" 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 { Link, NavLink } from 'react-router-dom';
import { LogoWithoutText } from '../icons/LogoWithoutTextIcon';
export function Footer() { export function Footer() {
return ( return (
<footer className="sm:grid xl:grid-cols-[2fr_1fr_1fr] sm:grid-cols-2 sm:max-xl:grid-rows-2 bg-[#14161F]"> <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"> <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={'/'}> <Link to={'/'}>
<LogoIcon /> <img
src="images/GRAFFinteractive.svg"
alt="GRAFFinteractive"
className="w-[118px] max-md:hidden"
/>
<div className="md:hidden">
<LogoWithoutText />
</div>
</Link> </Link>
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-1">
<div className="flex gap-x-4 m-text"> <div className="flex gap-x-4 m-text">
@@ -20,7 +27,7 @@ export function Footer() {
<Link to="https://graff.tech">graff.tech</Link> <Link to="https://graff.tech">graff.tech</Link>
</div> </div>
<p className="opacity-40 sm:font-medium m-text"> <p className="opacity-40 sm:font-medium m-text">
© 2024 GRAFF interactive. Все права защищены © 2026 GRAFF interactive. Все права защищены
</p> </p>
</div> </div>
</div> </div>
+4 -4
View File
@@ -1,5 +1,4 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { LogoIcon } from '../icons/LogoIcon';
import { Button } from '../ui/Button'; import { Button } from '../ui/Button';
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper'; import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
import { useModalStore } from '../../stores/modalStore'; import { useModalStore } from '../../stores/modalStore';
@@ -26,9 +25,10 @@ export function Header() {
return ( 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"> <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"> <Link to={'/'} className="max-sm:hidden">
<ClassNameWrapper <img
element={<LogoIcon />} src="images/GRAFFinteractive.svg"
className="h-8 lg:h-10 lg:w-[113px] w-[93px]" alt="GRAFFinteractive"
className="lg:w-[118px] sm:w-[80px] max-sm:hidden"
/> />
</Link> </Link>
<Link to={'/'} className="sm:hidden"> <Link to={'/'} className="sm:hidden">
+9 -3
View File
@@ -1,15 +1,16 @@
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper'; import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
import { useModalStore } from '../../stores/modalStore'; import { useModalStore } from '../../stores/modalStore';
import { Button } from '../ui/Button'; import { Button } from '../ui/Button';
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { ArrowRightIcon } from '../icons/ArrowRightIcon'; import { ArrowRightIcon } from '../icons/ArrowRightIcon';
import { CloseIcon } from '../icons/CloseIcon'; import { CloseIcon } from '../icons/CloseIcon';
import { FeedbackForm } from './FeedbackForm'; import { FeedbackForm } from './FeedbackForm';
import { useOnClickOutside } from 'usehooks-ts';
export function ModalWithForm() { export function ModalWithForm() {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
const [sent, setSent] = useState(false); const [sent, setSent] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const listener = (e: KeyboardEvent) => { const listener = (e: KeyboardEvent) => {
@@ -22,8 +23,13 @@ export function ModalWithForm() {
return () => document.removeEventListener('keydown', listener); return () => document.removeEventListener('keydown', listener);
}, [setModal]); }, [setModal]);
useOnClickOutside(modalRef, () => setModal(false));
return ( 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 ? ( {!sent ? (
<div className="space-y-8"> <div className="space-y-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
+1 -1
View File
@@ -1,7 +1,7 @@
export function LogoIcon() { export function LogoIcon() {
return ( return (
<svg <svg
width={128} width={138}
height={50} height={50}
viewBox="0 0 128 50" viewBox="0 0 128 50"
fill="none" 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; disabled?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
/** Native link (mailto:, tel:, https://). More reliable than JS navigation on some mobile WebViews. */
href?: string;
} }
export function Button({ export function Button({
@@ -18,25 +20,47 @@ export function Button({
disabled = false, disabled = false,
className, className,
onClick, onClick,
href,
}: ButtonProps) { }: ButtonProps) {
return ( const sharedClassName = `group relative px-6 py-2 rounded-full min-w-fit ${
<button (color === 'primary'
disabled={disabled} ? 'bg-gradient-to-r from-[#798FFF] to-[#D375FF]'
onClick={onClick} : '') ||
className={`group relative px-6 py-2 rounded-full min-w-fit ${ (color === 'secondary' ? 'outline outline-1 outline-[#3D425C]' : '')
(color === 'primary' } ${
? 'bg-gradient-to-r from-[#798FFF] to-[#D375FF]' icon ? 'pr-4' : ''
: '') || } flex gap-1 items-center overflow-hidden w-${width} ${className} justify-between`;
(color === 'secondary' ? 'outline outline-1 outline-[#3D425C]' : '')
} ${ const inner = (
icon ? 'pr-4' : '' <>
} flex gap-1 items-center overflow-hidden w-${width} ${className} justify-between`}
>
<span className="group-hover:opacity-10 opacity-0 bg-black transition-opacity absolute top-0 left-0 w-full h-full"></span> <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')}> <span className={'relative font-medium' + (icon ? '' : ' m-auto')}>
{children} {children}
</span> </span>
<span className="relative">{icon}</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> </button>
); );
} }
+2 -2
View File
@@ -27,13 +27,13 @@ export const stands: IStand[] = [
}, },
{ {
img: 'images/stands/interactive_floor.jpg', img: 'images/stands/interactive_floor.jpg',
title: 'Interactive floor', title: 'Интерактивный этаж',
annotation: 'Выставка «Россия ВДНХ»', annotation: 'Выставка «Россия ВДНХ»',
year: '2022', year: '2022',
}, },
{ {
img: 'images/stands/graff_estate_stream.jpg', img: 'images/stands/graff_estate_stream.jpg',
title: 'Graff.estate stream', title: 'Модуль удаленной демонстрации недвижимости GRAFF.estate',
annotation: 'WOW FEST', annotation: 'WOW FEST',
year: '2023', year: '2023',
}, },