upd
@@ -5,6 +5,7 @@
|
|||||||
"name": "dyagilev",
|
"name": "dyagilev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@number-flow/react": "^0.6.0",
|
"@number-flow/react": "^0.6.0",
|
||||||
|
"@yandex/ymaps3-types": "^1.0.19487230",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
@@ -262,6 +263,8 @@
|
|||||||
|
|
||||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||||
|
|
||||||
|
"@yandex/ymaps3-types": ["@yandex/ymaps3-types@1.0.19487230", "", { "peerDependencies": { "@types/react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@vue/runtime-core": "3" }, "optionalPeers": ["@types/react", "@types/react-dom", "@vue/runtime-core"] }, "sha512-jgryA+TzR/yJ6o3mXHOmalDKEefzi8w/VSAbSzk4uVrn9JzDxXq5i5OKnCVixQbpoxB1lbGJDpvU4fdImRaNww=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
"name": "dyagilev",
|
"name": "dyagilev",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@number-flow/react": "^0.6.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"motion": "^12.38.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"react-pageflip": "^2.0.3",
|
||||||
|
"zustand": "^5.0.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@@ -1300,6 +1305,20 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@number-flow/react": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@number-flow/react/-/react-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-77Yfc9+zkV2UDSP8phhZzxJGuwxi/Tt1TikmipL+1r3e9GFKEYDZ1XwInj67NoSt3OnOB0KLvvcl3lfPZgBHVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.1.4",
|
||||||
|
"number-flow": "0.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19",
|
||||||
|
"react-dom": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -1362,7 +1381,7 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -2558,6 +2577,15 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -2634,7 +2662,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
@@ -3367,6 +3395,12 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esm-env": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
@@ -3586,6 +3620,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.38.0",
|
||||||
|
"motion-utils": "^12.36.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -4661,6 +4722,47 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.36.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -4838,6 +4940,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/number-flow": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5039,6 +5150,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/page-flip": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/page-flip/-/page-flip-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-96lQFUUz7r/LZzEUZJ3yBIMEKU9+m8HMFDzTvTdD6P7Ag/wXINjp9n0W7b4wanwnDbQETo4uNUoL3zMqpFxwGA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -5394,6 +5511,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-pageflip": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-pageflip/-/react-pageflip-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-k81mHhRvUM52y8jyzTCh5t4O0lepkLhp+XGSUzq2C3uD+iW99Cv0jfRlqFCjZbD5N3jKkIFr7/3giucoXKDP3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"page-flip": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -6686,6 +6812,35 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz",
|
||||||
|
"integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@number-flow/react": "^0.6.0",
|
"@number-flow/react": "^0.6.0",
|
||||||
|
"@yandex/ymaps3-types": "^1.0.19487230",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 806 KiB |
|
After Width: | Height: | Size: 772 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -19,6 +19,14 @@ body {
|
|||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Typography */
|
/* Typography */
|
||||||
.line-xl {
|
.line-xl {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`h-full antialiased scroll-smooth text-white ${manrope.className}`}
|
className={`h-full no-scrollbar antialiased scroll-smooth text-white ${manrope.className}`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full">{children}</body>
|
<body className="min-h-full">{children}</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ import MenuSidebar from "@/components/ui/MenuSidebar";
|
|||||||
import Overview from "@/components/pages/Overview";
|
import Overview from "@/components/pages/Overview";
|
||||||
import ScreenOverlay from "@/components/ui/ScreenOverlay";
|
import ScreenOverlay from "@/components/ui/ScreenOverlay";
|
||||||
import Premiere from "@/components/pages/Premiere";
|
import Premiere from "@/components/pages/Premiere";
|
||||||
|
import Architecture from "@/components/pages/Architecture";
|
||||||
|
import Location from "@/components/pages/Location";
|
||||||
|
import MapPage from "@/components/pages/MapPage";
|
||||||
|
import ResidentialForm from "@/components/pages/ResidentialForm";
|
||||||
|
import Compromises from "@/components/pages/Compromises";
|
||||||
|
import LifeAndWork from "@/components/pages/LifeAndWork";
|
||||||
|
import Residents from "@/components/pages/Residents";
|
||||||
|
import Collection from "@/components/pages/Collection/Collection";
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
return (
|
return (
|
||||||
<main className="relative overflow-hidden ">
|
<main className="relative">
|
||||||
<LoadingScreen />
|
<LoadingScreen />
|
||||||
|
|
||||||
{/* Основной UI */}
|
{/* Основной UI */}
|
||||||
@@ -22,6 +30,14 @@ export default function Root() {
|
|||||||
<Hero />
|
<Hero />
|
||||||
<Overview />
|
<Overview />
|
||||||
<Premiere />
|
<Premiere />
|
||||||
|
<Architecture />
|
||||||
|
<Location />
|
||||||
|
<MapPage />
|
||||||
|
<ResidentialForm />
|
||||||
|
<Compromises />
|
||||||
|
<LifeAndWork />
|
||||||
|
<Residents />
|
||||||
|
<Collection />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { YMapZoomControl } from "@yandex/ymaps3-types/packages/controls";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ymaps3: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Map() {
|
||||||
|
const [mapComponents, setMapComponents] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initMap = async () => {
|
||||||
|
try {
|
||||||
|
if (!window.ymaps3) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
|
||||||
|
script.src =
|
||||||
|
"https://api-maps.yandex.ru/v3/?apikey=be3b1d29-3c2b-408b-8653-2cb448b80da1&lang=ru_RU";
|
||||||
|
|
||||||
|
script.async = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
script.onload = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.ymaps3.ready;
|
||||||
|
|
||||||
|
const ymaps3React = await window.ymaps3.import(
|
||||||
|
"@yandex/ymaps3-reactify"
|
||||||
|
);
|
||||||
|
const reactify = ymaps3React.reactify.bindTo(React, ReactDOM);
|
||||||
|
const components = reactify.module(window.ymaps3);
|
||||||
|
|
||||||
|
console.log("components", components);
|
||||||
|
|
||||||
|
setMapComponents(components);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initMap();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mapComponents) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
YMap,
|
||||||
|
YMapDefaultSchemeLayer,
|
||||||
|
YMapDefaultFeaturesLayer,
|
||||||
|
YMapScaleControl,
|
||||||
|
} = mapComponents;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YMap
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
location={{
|
||||||
|
center: [47.243739, 56.137059],
|
||||||
|
zoom: 15,
|
||||||
|
}}
|
||||||
|
behaviors={["drag", "dblClick"]}
|
||||||
|
>
|
||||||
|
<YMapDefaultSchemeLayer />
|
||||||
|
<YMapDefaultFeaturesLayer />
|
||||||
|
<YMapScaleControl />
|
||||||
|
</YMap>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Hint from "../ui/Hint";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
|
export default function Architecture() {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
id="architecture"
|
||||||
|
className="bg-[url('/img/bg/sky.png')]"
|
||||||
|
headerColorScheme="Light"
|
||||||
|
>
|
||||||
|
<h2 className="text-[5.556vw] leading-[1] text-white w-full font-light flex flex-col gap-[0.556vw] select-none z-[2] relative">
|
||||||
|
<span>
|
||||||
|
Архитектура, говорящая <br /> на языке города
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="lg:size-[2.778vw] absolute top-[33.1vw] left-[48.1vw] z-[2]">
|
||||||
|
<Hint
|
||||||
|
openByDefault
|
||||||
|
direction="right"
|
||||||
|
title={"Скругленные\u00A0углы"}
|
||||||
|
subtitle={
|
||||||
|
"Развивающее\u00A0пространство\nс\u00A0безопасными\u00A0материалами"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:size-[2.778vw] absolute top-[23.1vw] left-[68.1vw] z-[2]">
|
||||||
|
<Hint title={"Еще какой-то угол"} subtitle={"Смотри - как настоящий"} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/img/mocks/building2.png"
|
||||||
|
alt="Architecture"
|
||||||
|
className="absolute bottom-0 left-0 w-full h-full object-cover z-[1]"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type AppartamentDescriptionProps = {
|
||||||
|
title: string;
|
||||||
|
label: string;
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppartamentDescription({
|
||||||
|
title,
|
||||||
|
label,
|
||||||
|
tags,
|
||||||
|
}: AppartamentDescriptionProps) {
|
||||||
|
const animationKey = `${label}-${title}-${tags.join("-")}`;
|
||||||
|
const animationDuration = 0.25;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={animationKey}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={{
|
||||||
|
hidden: {},
|
||||||
|
visible: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.06,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className="caption-m text-[#242424]/40"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 12 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</motion.span>
|
||||||
|
<motion.h2
|
||||||
|
className="title-l whitespace-pre-line text-[#262626] mt-[0.833vw] mb-[2.778vw]"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 16 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-[0.556vw]">
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<motion.span
|
||||||
|
className="text-m text-[#242424]/40 py-[0.833vw] px-[1.389vw] border border-[#262626]/6 rounded-full w-max "
|
||||||
|
key={`${tag}-${index}`}
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 12 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import Section from "../../ui/Section";
|
||||||
|
import { ANIMATION_DURATION, SwipeState } from "./helpers";
|
||||||
|
import CursorButtonWrapper from "../../ui/CursorButtonWrapper";
|
||||||
|
import {
|
||||||
|
useScroll,
|
||||||
|
useMotionValueEvent,
|
||||||
|
motion,
|
||||||
|
AnimatePresence,
|
||||||
|
} from "framer-motion";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import TinderSlotCard from "./TinderSlotCard";
|
||||||
|
import CollectionCard from "./CollectionCard";
|
||||||
|
import { useCollectionScrollCardMotion } from "./useCollectionScrollCardMotion";
|
||||||
|
import AppartamentDescription from "./AppartamentDescription";
|
||||||
|
import PlanDescription from "./PlanDescription";
|
||||||
|
import { COLLECTION_DATA, getInitialCollectionSlotIndices } from "./data";
|
||||||
|
|
||||||
|
const DECK_LENGTH = COLLECTION_DATA.length;
|
||||||
|
|
||||||
|
function rotateIndicesRight(prev: number[]) {
|
||||||
|
if (DECK_LENGTH <= 0) return prev;
|
||||||
|
return prev.map((i) => (i - 1 + DECK_LENGTH) % DECK_LENGTH);
|
||||||
|
}
|
||||||
|
function rotateIndicesLeft(prev: number[]) {
|
||||||
|
if (DECK_LENGTH <= 0) return prev;
|
||||||
|
return prev.map((i) => (i + 1) % DECK_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Collection() {
|
||||||
|
// Индекс изображения которое считается активным по умолчанию.
|
||||||
|
// Указано X - значит imagesOrder[X] - активный
|
||||||
|
const COLLECTION_ACTIVE_SLOT_INDEX = 1;
|
||||||
|
|
||||||
|
// Состояния
|
||||||
|
const isAnimating = useRef(false);
|
||||||
|
const [isTinder, setIsTinder] = useState(false);
|
||||||
|
|
||||||
|
// Отслеживание скролла
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
useMotionValueEvent(scrollYProgress, "change", (latest) => {
|
||||||
|
setIsTinder(latest >= 0.8);
|
||||||
|
});
|
||||||
|
|
||||||
|
// по сути, порядок изображений, которые сдивгается влево или вправо при листании,
|
||||||
|
// в результате чего обновялются отображаемые фото.
|
||||||
|
// Нужен для сохранения порядка изображения после выхода из состояния тиндера.
|
||||||
|
const [imagesOrder, setImagesOrder] = useState(
|
||||||
|
getInitialCollectionSlotIndices
|
||||||
|
);
|
||||||
|
|
||||||
|
// Состояние анимации свайпа. Меняется между idle и animating.
|
||||||
|
// idle - нет анимации, animating - анимация свайпа.
|
||||||
|
const [swipe, setSwipe] = useState<SwipeState>({ phase: "idle" });
|
||||||
|
|
||||||
|
// Индекс активного изображения (по середине). Меняется сразу при клике,
|
||||||
|
// а imagesOrder обновляется позже, после завершения анимации карточек.
|
||||||
|
const [activeRecordIndex, setActiveRecordIndex] = useState(() => {
|
||||||
|
if (DECK_LENGTH === 0) return 0;
|
||||||
|
return imagesOrder[COLLECTION_ACTIVE_SLOT_INDEX] % DECK_LENGTH;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Активный элемент коллекции.
|
||||||
|
// Содержит информацию о квартире
|
||||||
|
const activeItem =
|
||||||
|
DECK_LENGTH > 0 ? COLLECTION_DATA[activeRecordIndex] : null;
|
||||||
|
|
||||||
|
// Анимация свайпа.
|
||||||
|
// Спустя ANIMATION_DURATION меняем изображения путем шифта индексов в imagesOrder
|
||||||
|
// Нужен таймер для того, чтобы анимация свайпа
|
||||||
|
useEffect(() => {
|
||||||
|
if (swipe.phase !== "animating") return;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setImagesOrder((prev) =>
|
||||||
|
swipe.dir === "right"
|
||||||
|
? rotateIndicesRight(prev)
|
||||||
|
: rotateIndicesLeft(prev)
|
||||||
|
);
|
||||||
|
|
||||||
|
setSwipe({ phase: "idle" });
|
||||||
|
isAnimating.current = false;
|
||||||
|
});
|
||||||
|
}, ANIMATION_DURATION * 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [swipe]);
|
||||||
|
|
||||||
|
// Motion-значения в `vw` для трёх карточек коллекции при скролле.
|
||||||
|
const scrollCards = useCollectionScrollCardMotion(scrollYProgress);
|
||||||
|
|
||||||
|
const handleSliderClick = (direction: "right" | "left") => {
|
||||||
|
if (!isTinder || isAnimating.current || DECK_LENGTH === 0) return;
|
||||||
|
const nextOrder =
|
||||||
|
direction === "right"
|
||||||
|
? rotateIndicesRight(imagesOrder)
|
||||||
|
: rotateIndicesLeft(imagesOrder);
|
||||||
|
setActiveRecordIndex(nextOrder[COLLECTION_ACTIVE_SLOT_INDEX] % DECK_LENGTH);
|
||||||
|
isAnimating.current = true;
|
||||||
|
setSwipe({ phase: "animating", dir: direction });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImgSrc = (slotIndex: number) => {
|
||||||
|
if (DECK_LENGTH === 0) return "";
|
||||||
|
return COLLECTION_DATA[imagesOrder[slotIndex]].src;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTinderImgSrc = (slotIndex: number) => {
|
||||||
|
if (DECK_LENGTH === 0) return "";
|
||||||
|
|
||||||
|
// фиксит баг с мерцающей фотографией что
|
||||||
|
if (
|
||||||
|
swipe.phase === "animating" &&
|
||||||
|
swipe.dir === "right" &&
|
||||||
|
slotIndex === 3
|
||||||
|
) {
|
||||||
|
return COLLECTION_DATA[rotateIndicesRight(imagesOrder)[0]].src;
|
||||||
|
}
|
||||||
|
return getImgSrc(slotIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (DECK_LENGTH === 0) {
|
||||||
|
return (
|
||||||
|
<Section id="collection" headerColorScheme="Dark">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="relative min-h-[30vh] px-[calc(15.042vw-1.667vw)]"
|
||||||
|
>
|
||||||
|
<p className="text-s text-white/60">Нет данных коллекции</p>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section id="collection" headerColorScheme="Dark">
|
||||||
|
<h2 className="text-[5.556vw] lg:px-[calc(15.042vw-1.667vw)] lg:mb-[2.778vw] leading-[1] w-full font-light flex flex-col gap-[0.556vw] select-none">
|
||||||
|
<span className="block">Коллекция авторских</span>
|
||||||
|
<span className="block ml-auto">резиденций</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div ref={ref} className="relative h-[200vh]">
|
||||||
|
<div className="sticky top-0 w-full h-[100vh] ">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{isTinder && activeItem && (
|
||||||
|
<motion.div
|
||||||
|
key="description"
|
||||||
|
initial={{ opacity: 0, x: -100 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -150 }}
|
||||||
|
className="lg:pt-[10vw] lg:pb-[5.417vw] h-full w-[22.917vw] absolute left-0"
|
||||||
|
>
|
||||||
|
<AppartamentDescription
|
||||||
|
title={activeItem.title}
|
||||||
|
label={activeItem.label}
|
||||||
|
tags={activeItem.tags}
|
||||||
|
/>
|
||||||
|
<span className="title-l text-[#242424]/40 absolute bottom-[5.417vw] right-0">
|
||||||
|
{activeRecordIndex + 1}/{DECK_LENGTH}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{isTinder && activeItem && (
|
||||||
|
<motion.div
|
||||||
|
key="plan"
|
||||||
|
initial={{ opacity: 0, x: 100 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 150 }}
|
||||||
|
className="lg:pt-[10vw] lg:pb-[5.417vw] h-full w-[22.917vw] absolute right-0"
|
||||||
|
>
|
||||||
|
<PlanDescription
|
||||||
|
planSrc={activeItem.planSrc}
|
||||||
|
description={activeItem.planDescription}
|
||||||
|
onExploreClick={() => {}}
|
||||||
|
onShowVariantsClick={() => {}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{isTinder ? (
|
||||||
|
<>
|
||||||
|
{[0, 1, 2, 3].map((index) => (
|
||||||
|
<TinderSlotCard
|
||||||
|
key={index}
|
||||||
|
indexInDeck={index}
|
||||||
|
swipe={swipe}
|
||||||
|
src={getTinderImgSrc(index)}
|
||||||
|
className="-translate-y-1/2"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CollectionCard
|
||||||
|
zIndex={2}
|
||||||
|
w={scrollCards[0].widthVw}
|
||||||
|
h={scrollCards[0].heightVw}
|
||||||
|
l={scrollCards[0].leftVw}
|
||||||
|
src={getImgSrc(0)}
|
||||||
|
className="-translate-y-1/2"
|
||||||
|
/>
|
||||||
|
<CollectionCard
|
||||||
|
zIndex={3}
|
||||||
|
w={scrollCards[1].widthVw}
|
||||||
|
h={scrollCards[1].heightVw}
|
||||||
|
l={scrollCards[1].leftVw}
|
||||||
|
src={getImgSrc(1)}
|
||||||
|
className="-translate-y-1/2"
|
||||||
|
/>
|
||||||
|
<CollectionCard
|
||||||
|
zIndex={2}
|
||||||
|
w={scrollCards[2].widthVw}
|
||||||
|
h={scrollCards[2].heightVw}
|
||||||
|
l={scrollCards[2].leftVw}
|
||||||
|
src={getImgSrc(2)}
|
||||||
|
className="-translate-y-1/2"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isTinder && (
|
||||||
|
<CursorButtonWrapper
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[31.111vw] h-[41.667vw] z-[4]"
|
||||||
|
onRightClick={() => handleSliderClick("left")}
|
||||||
|
onLeftClick={() => handleSliderClick("right")}
|
||||||
|
>
|
||||||
|
<div className="size-full" />
|
||||||
|
</CursorButtonWrapper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import { motion, MotionValue } from "motion/react";
|
||||||
|
import { ANIMATION_DURATION } from "./helpers";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
export default function CollectionCard({
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
l,
|
||||||
|
src,
|
||||||
|
className,
|
||||||
|
zIndex,
|
||||||
|
}: {
|
||||||
|
w: MotionValue<string> | string;
|
||||||
|
h: MotionValue<string> | string;
|
||||||
|
l: MotionValue<string> | string;
|
||||||
|
src: string;
|
||||||
|
className?: string;
|
||||||
|
zIndex?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
left: l,
|
||||||
|
zIndex: zIndex,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: ANIMATION_DURATION,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
className={clsx("absolute top-1/2", className)}
|
||||||
|
>
|
||||||
|
<img src={src} alt="" className="size-full object-cover" />
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import ButtonRound from "@/components/ui/Buttons/ButtonRound";
|
||||||
|
import Tour3D from "@/components/icons/Tour3D";
|
||||||
|
import Search from "@/components/icons/Search";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
type PlanDescriptionProps = {
|
||||||
|
planSrc: string;
|
||||||
|
description: string;
|
||||||
|
onExploreClick: () => void;
|
||||||
|
onShowVariantsClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PlanDescription({
|
||||||
|
planSrc,
|
||||||
|
description,
|
||||||
|
onExploreClick,
|
||||||
|
onShowVariantsClick,
|
||||||
|
}: PlanDescriptionProps) {
|
||||||
|
const animationKey = `${planSrc}-${description}`;
|
||||||
|
const animationDuration = 0.25;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start gap-2 w-full h-full">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={animationKey}
|
||||||
|
className="flex flex-col items-start gap-2 w-full h-full"
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={{
|
||||||
|
hidden: {},
|
||||||
|
visible: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.06,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-start gap-2 ">
|
||||||
|
<motion.div
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 12 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<ButtonRound
|
||||||
|
type="Dark"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
onClick={onExploreClick}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 12 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<ButtonRound
|
||||||
|
type="Dark"
|
||||||
|
text={"Показать\nварианты"}
|
||||||
|
onClick={onShowVariantsClick}
|
||||||
|
icon={<Search />}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.img
|
||||||
|
src={planSrc}
|
||||||
|
alt="plan"
|
||||||
|
className="size-[15vw] object-cover mb-[2.083vw] mt-auto"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 16 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
<motion.p
|
||||||
|
className="text-s text-[#262626] w-[85%]"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 12 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
transition={{ duration: animationDuration, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { ANIMATION_DURATION, SwipeState } from "./helpers";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
|
||||||
|
type CardRole = "middle" | "right" | "left" | "under";
|
||||||
|
const BASE_ROLES: CardRole[] = ["left", "middle", "right", "under"]; // Базовое положение
|
||||||
|
const TARGET_RIGHT: CardRole[] = ["middle", "right", "under", "left"]; // Положение после свайпа вправо
|
||||||
|
const TARGET_LEFT: CardRole[] = ["under", "left", "middle", "right"]; // Положение после свайпа вправо
|
||||||
|
|
||||||
|
// Стили положения для разных состояний карточки
|
||||||
|
const CARDS_STATE = {
|
||||||
|
right: {
|
||||||
|
width: "24.797vw",
|
||||||
|
height: "32.528vw",
|
||||||
|
left: "40vw",
|
||||||
|
zIndex: 2,
|
||||||
|
},
|
||||||
|
middle: {
|
||||||
|
width: "27.275vw",
|
||||||
|
height: "35.139vw",
|
||||||
|
left: "34.8vw",
|
||||||
|
zIndex: 3,
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
width: "24.797vw",
|
||||||
|
height: "32.528vw",
|
||||||
|
left: "32.222vw",
|
||||||
|
zIndex: 2,
|
||||||
|
},
|
||||||
|
under: {
|
||||||
|
width: "14.931vw",
|
||||||
|
height: "19.583vw",
|
||||||
|
left: "40.708vw",
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const Z_INDEX_DELAY_MS = (ANIMATION_DURATION * 1000) / 2;
|
||||||
|
|
||||||
|
export default function TinderSlotCard({
|
||||||
|
indexInDeck,
|
||||||
|
swipe,
|
||||||
|
src,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
indexInDeck: number;
|
||||||
|
swipe: SwipeState;
|
||||||
|
src: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
function cardPosition(slotIndex: number, swipe: SwipeState) {
|
||||||
|
if (swipe.phase !== "animating") return CARDS_STATE[BASE_ROLES[slotIndex]];
|
||||||
|
|
||||||
|
const keys = swipe.dir === "right" ? TARGET_RIGHT : TARGET_LEFT;
|
||||||
|
return CARDS_STATE[keys[slotIndex]];
|
||||||
|
}
|
||||||
|
// Переход из состояния
|
||||||
|
const baseGeo = cardPosition(indexInDeck, { phase: "idle" });
|
||||||
|
// в состояние
|
||||||
|
const geo = cardPosition(indexInDeck, swipe);
|
||||||
|
|
||||||
|
const swipeDir = swipe.phase === "animating" ? swipe.dir : undefined;
|
||||||
|
|
||||||
|
/** В idle z-index из геометрии слота — без кадра с «чужим» delayedZ */
|
||||||
|
const [delayedZ, setDelayedZ] = useState(() => baseGeo.zIndex);
|
||||||
|
const zForMotion = swipe.phase === "idle" ? geo.zIndex : delayedZ;
|
||||||
|
const zHalfTimerRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (swipe.phase !== "animating") return;
|
||||||
|
|
||||||
|
const fromZ = baseGeo.zIndex;
|
||||||
|
const toZ = geo.zIndex;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
setDelayedZ(fromZ);
|
||||||
|
|
||||||
|
zHalfTimerRef.current = window.setTimeout(() => {
|
||||||
|
if (!cancelled) setDelayedZ(toZ);
|
||||||
|
zHalfTimerRef.current = undefined;
|
||||||
|
}, Z_INDEX_DELAY_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (zHalfTimerRef.current !== undefined) {
|
||||||
|
clearTimeout(zHalfTimerRef.current);
|
||||||
|
zHalfTimerRef.current = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [swipe.phase, swipeDir, indexInDeck, geo.zIndex, baseGeo.zIndex]);
|
||||||
|
|
||||||
|
const transition =
|
||||||
|
swipe.phase === "animating"
|
||||||
|
? {
|
||||||
|
duration: ANIMATION_DURATION,
|
||||||
|
ease: "easeInOut" as const,
|
||||||
|
zIndex: { duration: 0 },
|
||||||
|
}
|
||||||
|
: { duration: 0 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
width: geo.width,
|
||||||
|
height: geo.height,
|
||||||
|
left: geo.left,
|
||||||
|
zIndex: zForMotion,
|
||||||
|
}}
|
||||||
|
transition={transition}
|
||||||
|
className={clsx(
|
||||||
|
"absolute top-1/2",
|
||||||
|
swipe.phase === "animating" &&
|
||||||
|
"will-change-[transform,width,height,left]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
decoding="async"
|
||||||
|
draggable={false}
|
||||||
|
className="size-full object-cover pointer-events-none select-none"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
export type CollectionData = {
|
||||||
|
title: string;
|
||||||
|
label: string;
|
||||||
|
tags: string[];
|
||||||
|
/** Основное фото карточки в колоде */
|
||||||
|
src: string;
|
||||||
|
/** План в блоке справа */
|
||||||
|
planSrc: string;
|
||||||
|
planDescription: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const COLLECTION_DATA: CollectionData[] = [
|
||||||
|
{
|
||||||
|
title: "Однокомнатная видовая квартира до 44м² 1",
|
||||||
|
label: "Соло в центре событий",
|
||||||
|
tags: ["потолок 3,9м", "панорамное остекление", "матер-спальня"],
|
||||||
|
src: "/img/collection/1.png",
|
||||||
|
planSrc: "/img/collection/1.png",
|
||||||
|
planDescription: " Компактный и функциональный лот, созданный для тех, кто ценит ритм города
и личную свободу. Пространство, где каждый метр работает на ваш комфорт,
не отвлекая от главного.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Однокомнатная видовая квартира до 44м² 2",
|
||||||
|
label: "Соло в центре событий",
|
||||||
|
tags: ["потолок 3,9м", "панорамное остекление", "матер-спальня"],
|
||||||
|
src: "/img/collection/2.png",
|
||||||
|
planSrc: "/img/collection/2.png",
|
||||||
|
planDescription: " Компактный и функциональный лот, созданный для тех, кто ценит ритм города
и личную свободу. Пространство, где каждый метр работает на ваш комфорт,
не отвлекая от главного.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Однокомнатная видовая квартира до 44м² 3",
|
||||||
|
label: "Соло в центре событий",
|
||||||
|
tags: ["потолок 3,9м", "панорамное остекление", "матер-спальня"],
|
||||||
|
src: "/img/collection/3.png",
|
||||||
|
planSrc: "/img/collection/3.png",
|
||||||
|
planDescription: " Компактный и функциональный лот, созданный для тех, кто ценит ритм города
и личную свободу. Пространство, где каждый метр работает на ваш комфорт,
не отвлекая от главного.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Однокомнатная видовая квартира до 44м² 4",
|
||||||
|
label: "Соло в центре событий",
|
||||||
|
tags: ["потолок 3,9м", "панорамное остекление", "матер-спальня"],
|
||||||
|
src: "/img/collection/4.png",
|
||||||
|
planSrc: "/img/collection/4.png",
|
||||||
|
planDescription: " Компактный и функциональный лот, созданный для тех, кто ценит ритм города
и личную свободу. Пространство, где каждый метр работает на ваш комфорт,
не отвлекая от главного.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Однокомнатная видовая квартира до 44м² 5",
|
||||||
|
label: "Соло в центре событий",
|
||||||
|
tags: ["потолок 3,9м", "панорамное остекление", "матер-спальня"],
|
||||||
|
src: "/img/collection/5.png",
|
||||||
|
planSrc: "/img/collection/5.png",
|
||||||
|
planDescription: " Компактный и функциональный лот, созданный для тех, кто ценит ритм города
и личную свободу. Пространство, где каждый метр работает на ваш комфорт,
не отвлекая от главного.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Порядок объектов по умолчанию. Объект по индексу 1 - активный.
|
||||||
|
// Поэтому начинаем с последнего. N-1, 0, 1, 2...
|
||||||
|
export function getInitialCollectionSlotIndices(): number[] {
|
||||||
|
const n = COLLECTION_DATA.length;
|
||||||
|
|
||||||
|
if(n < 3)
|
||||||
|
return [0, 0, 0, 0]
|
||||||
|
else{
|
||||||
|
const first = n - 1;
|
||||||
|
const second = new Array(n - 1).fill(0).map((_, i) => i);
|
||||||
|
return [first, 0, 1, 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const ANIMATION_DURATION = 0.5;
|
||||||
|
|
||||||
|
export type SwipeState =
|
||||||
|
| { phase: "idle" }
|
||||||
|
| { phase: "animating"; dir: "left" | "right" };
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { type MotionValue, useTransform } from "framer-motion";
|
||||||
|
|
||||||
|
/** Диапазон прогресса скролла секции (как в useScroll offset) */
|
||||||
|
const SCROLL_INPUT: [number, number] = [0, 0.8];
|
||||||
|
|
||||||
|
/** Тройки [начало, конец] для left / width / height по каждой из трёх карточек вне тиндера */
|
||||||
|
export const COLLECTION_SCROLL_CARD_SPECS: Array<{
|
||||||
|
left: [number, number];
|
||||||
|
width: [number, number];
|
||||||
|
height: [number, number];
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
left: [24.514, 32.222],
|
||||||
|
width: [14.931, 24.797], // размеры левой карточки начальные -> тиндер
|
||||||
|
height: [19.583, 32.528],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
left: [40.8, 34.8],
|
||||||
|
width: [14.931, 27.275], // размеры центральной карточки начальные -> тиндер
|
||||||
|
height: [19.583, 35.139],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
left: [57.222, 40],
|
||||||
|
width: [14.931, 24.797], // размеры правой карточки начальные -> тиндер
|
||||||
|
height: [19.583, 32.528],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type CollectionScrollCardMotion = {
|
||||||
|
leftVw: MotionValue<string>;
|
||||||
|
widthVw: MotionValue<string>;
|
||||||
|
heightVw: MotionValue<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useCardScrollVw(
|
||||||
|
scrollYProgress: MotionValue<number>,
|
||||||
|
spec: (typeof COLLECTION_SCROLL_CARD_SPECS)[number],
|
||||||
|
): CollectionScrollCardMotion {
|
||||||
|
const left = useTransform(scrollYProgress, SCROLL_INPUT, spec.left);
|
||||||
|
const width = useTransform(scrollYProgress, SCROLL_INPUT, spec.width);
|
||||||
|
const height = useTransform(scrollYProgress, SCROLL_INPUT, spec.height);
|
||||||
|
const leftVw = useTransform(left, (v) => `${v}vw`);
|
||||||
|
const widthVw = useTransform(width, (v) => `${v}vw`);
|
||||||
|
const heightVw = useTransform(height, (v) => `${v}vw`);
|
||||||
|
return { leftVw, widthVw, heightVw };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Motion-значения в `vw` для трёх карточек коллекции при скролле (хуки — фиксированное число вызовов). */
|
||||||
|
export function useCollectionScrollCardMotion(
|
||||||
|
scrollYProgress: MotionValue<number>,
|
||||||
|
): readonly [
|
||||||
|
CollectionScrollCardMotion,
|
||||||
|
CollectionScrollCardMotion,
|
||||||
|
CollectionScrollCardMotion,
|
||||||
|
] {
|
||||||
|
const card0 = useCardScrollVw(scrollYProgress, COLLECTION_SCROLL_CARD_SPECS[0]);
|
||||||
|
const card1 = useCardScrollVw(scrollYProgress, COLLECTION_SCROLL_CARD_SPECS[1]);
|
||||||
|
const card2 = useCardScrollVw(scrollYProgress, COLLECTION_SCROLL_CARD_SPECS[2]);
|
||||||
|
return [card0, card1, card2] as const;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
import {
|
||||||
|
motion,
|
||||||
|
useMotionValueEvent,
|
||||||
|
useScroll,
|
||||||
|
useTransform,
|
||||||
|
} from "motion/react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||||
|
import VideoPlayer from "@/components/ui/VideoPlayer";
|
||||||
|
|
||||||
|
export default function Compromises() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const { setHeaderColorScheme } = useAppStateStore();
|
||||||
|
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
useMotionValueEvent(scrollYProgress, "change", (latest) => {
|
||||||
|
if (latest > 0.5) {
|
||||||
|
setExpanded(true);
|
||||||
|
setHeaderColorScheme("Light");
|
||||||
|
} else {
|
||||||
|
setExpanded(false);
|
||||||
|
setHeaderColorScheme("Dark");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoTop = useTransform(scrollYProgress, [0.2, 0.5], [13.567, 0]);
|
||||||
|
const videoLeft = useTransform(scrollYProgress, [0.2, 0.5], [26.181, 0]);
|
||||||
|
const videoWidth = useTransform(scrollYProgress, [0.2, 0.5], [47.139, 100]);
|
||||||
|
const videoHeight = useTransform(scrollYProgress, [0.2, 0.5], [43.125, 100]);
|
||||||
|
|
||||||
|
const videoTopVw = useTransform(videoTop, (v) => `${v}vw`);
|
||||||
|
const videoLeftVw = useTransform(videoLeft, (v) => `${v}vw`);
|
||||||
|
const videoWidthVw = useTransform(videoWidth, (v) => `${v}vw`);
|
||||||
|
const videoHeightVh = useTransform(videoHeight, (v) => `${v}vh`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
id="compromises"
|
||||||
|
headerColorScheme="Dark"
|
||||||
|
className="!h-[400vh] relative !px-0 !pb-0"
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<h2 className="text-[5.556vw] lg:px-[calc(11.042vw-1.667vw)] lg:mb-[2.778vw] leading-[1] w-full font-light flex flex-col gap-[0.556vw] select-none">
|
||||||
|
<span className="block">Дом для жизни</span>
|
||||||
|
<span className="block ml-auto">без компромиссов</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="sticky lg:top-[0vw] left-0 w-full h-[100vh] overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="/img/compromises/1.png"
|
||||||
|
className="absolute
|
||||||
|
lg:left-[26.181vw] lg:top-0 lg:w-[18.819vw] lg:aspect-[271/176]
|
||||||
|
"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/img/compromises/2.png"
|
||||||
|
className="absolute
|
||||||
|
lg:left-[1.667vw] lg:top-[8.472vw] lg:w-[23.125vw] lg:aspect-[333/268]
|
||||||
|
"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
top: videoTopVw,
|
||||||
|
left: videoLeftVw,
|
||||||
|
width: videoWidthVw,
|
||||||
|
height: videoHeightVh,
|
||||||
|
}}
|
||||||
|
className=" absolute z-[2]"
|
||||||
|
>
|
||||||
|
<VideoPlayer expanded={expanded} />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/img/compromises/4.png"
|
||||||
|
className="absolute
|
||||||
|
lg:top-[35.514vw] lg:left-[43.253vw] lg:h-[15.347vw]
|
||||||
|
"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/img/compromises/3.png"
|
||||||
|
className="absolute
|
||||||
|
lg:top-[23.083vw] lg:left-[74.708vw] lg:w-[23.125vw] lg:aspect-[333/320]
|
||||||
|
"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import React from "react";
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
return (
|
return (
|
||||||
<section
|
<Section
|
||||||
id="hero"
|
id="hero"
|
||||||
className="relative w-full h-screen bg-[url('/img/bg/sky.png')] bg-cover bg-center
|
className="bg-[url('/img/bg/sky.png')]"
|
||||||
lg:px-[1.667vw] lg:pt-[7.986vw]"
|
headerColorScheme="Light"
|
||||||
>
|
>
|
||||||
<h1 className="text-[7.778vw] leading-[0.7] w-full font-light flex flex-col gap-[0.556vw] select-none">
|
<h1 className="text-[7.778vw] leading-[0.7] w-full font-light flex flex-col gap-[0.556vw] select-none text-white">
|
||||||
<span className="block">Жизнь на высоте</span>
|
<span className="block">Жизнь на высоте</span>
|
||||||
<span className="block ml-auto">с видом на залив</span>
|
<span className="block ml-auto">с видом на залив</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -19,6 +19,6 @@ export default function Hero() {
|
|||||||
draggable={false}
|
draggable={false}
|
||||||
className="object-cover absolute bottom-0 left-1/2 -translate-x-1/2 w-[75%] select-none"
|
className="object-cover absolute bottom-0 left-1/2 -translate-x-1/2 w-[75%] select-none"
|
||||||
/>
|
/>
|
||||||
</section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import React from "react";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
import {
|
||||||
|
motion,
|
||||||
|
useMotionValueEvent,
|
||||||
|
useScroll,
|
||||||
|
useTransform,
|
||||||
|
} from "motion/react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||||
|
import ButtonRound from "../ui/Buttons/ButtonRound";
|
||||||
|
import Tour3D from "../icons/Tour3D";
|
||||||
|
|
||||||
|
export default function LifeAndWork() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [titleColor, setTitleColor] = useState<"White" | "Black">("White");
|
||||||
|
const { setHeaderColorScheme } = useAppStateStore();
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoTop = useTransform(scrollYProgress, [0.2, 1], [0, 23.333]);
|
||||||
|
const videoLeft = useTransform(scrollYProgress, [0.2, 1], [0, 9.861]);
|
||||||
|
const videoWidth = useTransform(scrollYProgress, [0.2, 1], [100, 47.639]);
|
||||||
|
const videoHeight = useTransform(scrollYProgress, [0.2, 1], [100, 59.375]);
|
||||||
|
|
||||||
|
const videoTopVw = useTransform(videoTop, (v) => `${v}vw`);
|
||||||
|
const videoLeftVw = useTransform(videoLeft, (v) => `${v}vw`);
|
||||||
|
const videoWidthVw = useTransform(videoWidth, (v) => `${v}vw`);
|
||||||
|
const videoHeightVh = useTransform(videoHeight, (v) => `${v}vh`);
|
||||||
|
|
||||||
|
useMotionValueEvent(scrollYProgress, "change", (latest) => {
|
||||||
|
if (latest < 0.3) {
|
||||||
|
setHeaderColorScheme("Light");
|
||||||
|
} else {
|
||||||
|
setHeaderColorScheme("Dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latest < 0.6) {
|
||||||
|
setTitleColor("White");
|
||||||
|
setExpanded(true);
|
||||||
|
} else {
|
||||||
|
setTitleColor("Black");
|
||||||
|
setExpanded(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
id="life-and-work"
|
||||||
|
headerColorScheme="Dark"
|
||||||
|
className="!px-0 !pt-0"
|
||||||
|
>
|
||||||
|
<div ref={ref} className="!h-[200vh] relative">
|
||||||
|
<div className="sticky top-0 left-0 w-full h-[120dvh] overflow-hidden lg:pt-[6.778vw]">
|
||||||
|
<h2
|
||||||
|
style={{ color: titleColor === "White" ? "#FFFFFF" : "#262626" }}
|
||||||
|
className="text-[5.556vw] lg:px-[calc(11.111vw)] leading-[1] w-full font-light flex flex-col gap-[0.556vw] select-none relative transition-colors duration-300 ease-in-out z-[3]"
|
||||||
|
>
|
||||||
|
<span className="block">Жизнь, работа и отдых</span>
|
||||||
|
<span className="block ml-auto">под одной крышей</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
top: videoTopVw,
|
||||||
|
left: videoLeftVw,
|
||||||
|
width: videoWidthVw,
|
||||||
|
height: videoHeightVh,
|
||||||
|
}}
|
||||||
|
className="absolute bottom-0 left-[58.889vw] z-[2]"
|
||||||
|
>
|
||||||
|
<img src="/img/lifeAndWork/1.png" alt="" className="size-full " />
|
||||||
|
|
||||||
|
<ButtonRound
|
||||||
|
className={`z-[3] !absolute bottom-[0.833vw] left-[0.833vw] transition-opacity duration-300 ease-in-out ${
|
||||||
|
expanded ? "opacity-0" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
type="White"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<p className="title-s absolute bottom-[5.2vw] left-[58.889vw] text-[#242424]/40">
|
||||||
|
<span className="text-[#262626]">Зона тихого отдыха:</span>{" "}
|
||||||
|
уединённые <br />
|
||||||
|
скамьи среди зелени, мягкий <br />
|
||||||
|
свет и шум листвы
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=" flex flex-col">
|
||||||
|
<div className="ml-auto mr-[9.861vw] flex items-center gap-[5.347vw] mb-[5vw]">
|
||||||
|
<p className="title-s text-[#242424]/40">
|
||||||
|
<span className="text-[#262626]">Зона тихого отдыха:</span>{" "}
|
||||||
|
уединённые <br />
|
||||||
|
скамьи среди зелени, мягкий <br />
|
||||||
|
свет и шум листвы
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src="/img/lifeAndWork/2.png"
|
||||||
|
alt=""
|
||||||
|
className="lg:w-[39.444vw] aspect-square z-[2]"
|
||||||
|
/>
|
||||||
|
<ButtonRound
|
||||||
|
className={`z-[3] !absolute bottom-[0.833vw] left-[0.833vw]`}
|
||||||
|
type="White"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-[1.667vw] flex items-start gap-[1.389vw]">
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src="/img/lifeAndWork/3.png"
|
||||||
|
alt=""
|
||||||
|
className="lg:w-[23.125vw] z-[2]"
|
||||||
|
/>
|
||||||
|
<ButtonRound
|
||||||
|
className={`z-[3] !absolute bottom-[0.833vw] left-[0.833vw]`}
|
||||||
|
type="White"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="title-s text-[#242424]/40">
|
||||||
|
<span className="text-[#262626]">Зона тихого отдыха:</span>{" "}
|
||||||
|
уединённые <br />
|
||||||
|
скамьи среди зелени, мягкий <br />
|
||||||
|
свет и шум листвы
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-[1.389vw] mr-[1.667vw] -mt-[15vw]">
|
||||||
|
<p className="title-s text-[#242424]/40">
|
||||||
|
<span className="text-[#262626]">Зона тихого отдыха:</span>{" "}
|
||||||
|
уединённые <br />
|
||||||
|
скамьи среди зелени, мягкий <br />
|
||||||
|
свет и шум листвы
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src="/img/lifeAndWork/4.png"
|
||||||
|
alt=""
|
||||||
|
className="lg:w-[39.444vw] z-[2]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ButtonRound
|
||||||
|
className={`z-[3] !absolute bottom-[0.833vw] left-[0.833vw]`}
|
||||||
|
type="White"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-[1.389vw] ml-[1.667vw] mt-[5vw]">
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src="/img/lifeAndWork/5.png"
|
||||||
|
alt=""
|
||||||
|
className="lg:w-[47.639vw] z-[2]"
|
||||||
|
/>
|
||||||
|
<ButtonRound
|
||||||
|
className={`z-[3] !absolute bottom-[0.833vw] left-[0.833vw]`}
|
||||||
|
type="White"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="title-s text-[#242424]/40">
|
||||||
|
<span className="text-[#262626]">Зона тихого отдыха:</span>{" "}
|
||||||
|
уединённые <br />
|
||||||
|
скамьи среди зелени, мягкий <br />
|
||||||
|
свет и шум листвы
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from "react";
|
||||||
|
import BookSlider from "../ui/BookSlider";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
|
export default function Location() {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
id="architecture"
|
||||||
|
className="bg-[url('/img/bg/building2.png')] overflow-hidden"
|
||||||
|
headerColorScheme="Light"
|
||||||
|
>
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:w-[55.556vw] lg:h-[33.889vw]">
|
||||||
|
<BookSlider>
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">{"(1) Локации"}</div>
|
||||||
|
<div className="text-black block font-light text-m mt-[25.431vw] ">
|
||||||
|
<span className="font-medium">Чебоксарский залив:</span> центр
|
||||||
|
притяжения города и естественное продолжение вашего двора. Место
|
||||||
|
для утренних пробежек вдоль воды или тихих вечерних прогулок под
|
||||||
|
шум прибоя
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage2.png')] bg-cover bg-center" />
|
||||||
|
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">{"(1) Локации"}</div>
|
||||||
|
<div className="text-black block font-light text-m mt-[25.431vw] ">
|
||||||
|
<span className="font-medium">Чебоксарский залив:</span> центр
|
||||||
|
притяжения города и естественное продолжение вашего двора. Место
|
||||||
|
для утренних пробежек вдоль воды или тихих вечерних прогулок под
|
||||||
|
шум прибоя
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage2.png')] bg-cover bg-center" />
|
||||||
|
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">{"(1) Локации"}</div>
|
||||||
|
<div className="text-black block font-light text-m mt-[25.431vw] ">
|
||||||
|
<span className="font-medium">Чебоксарский залив:</span> центр
|
||||||
|
притяжения города и естественное продолжение вашего двора. Место
|
||||||
|
для утренних пробежек вдоль воды или тихих вечерних прогулок под
|
||||||
|
шум прибоя
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage2.png')] bg-cover bg-center" />
|
||||||
|
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">{"(1) Локации"}</div>
|
||||||
|
<div className="text-black block font-light text-m mt-[25.431vw] ">
|
||||||
|
<span className="font-medium">Чебоксарский залив:</span> центр
|
||||||
|
притяжения города и естественное продолжение вашего двора. Место
|
||||||
|
для утренних пробежек вдоль воды или тихих вечерних прогулок под
|
||||||
|
шум прибоя
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage2.png')] bg-cover bg-center" />
|
||||||
|
</BookSlider>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Map from "@/components/map/Map";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
|
export default function MapPage() {
|
||||||
|
return (
|
||||||
|
<Section id="map" headerColorScheme="Dark" className="h-screen !p-0">
|
||||||
|
<Map />
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
|
||||||
import BookSlider from "../ui/BookSlider";
|
import BookSlider from "../ui/BookSlider";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
export default function Overview() {
|
export default function Overview() {
|
||||||
return (
|
return (
|
||||||
<section
|
<Section
|
||||||
id="overview"
|
id="overview"
|
||||||
className="relative w-full h-screen bg-[url('/img/bg/building.png')] bg-cover bg-center
|
className=" bg-[url('/img/bg/building.png')] overflow-hidden"
|
||||||
lg:px-[1.667vw]"
|
headerColorScheme="Light"
|
||||||
>
|
>
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:w-[55.556vw] lg:h-[33.889vw]">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:w-[55.556vw] lg:h-[33.889vw]">
|
||||||
<BookSlider>
|
<BookSlider>
|
||||||
@@ -56,6 +56,6 @@ export default function Overview() {
|
|||||||
<div className="bg-[url('/img/mocks/bookPage.png')] bg-cover bg-center" />
|
<div className="bg-[url('/img/mocks/bookPage.png')] bg-cover bg-center" />
|
||||||
</BookSlider>
|
</BookSlider>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useAppStateStore } from "@/stores/useAppStateStore";
|
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
export default function Premiere() {
|
export default function Premiere() {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -23,14 +24,10 @@ export default function Premiere() {
|
|||||||
}, [setHeaderColorScheme]);
|
}, [setHeaderColorScheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<Section id="premiere" headerColorScheme="Dark">
|
||||||
id="premiere"
|
|
||||||
className="relative w-full min-h-screen text-black bg-white bg-cover bg-center
|
|
||||||
lg:px-[1.667vw] lg:pt-[7.778vw] lg:pb-[9.444vw]"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="absolute top-[95dvh] left-0 w-full h-[calc(100%-95dvh)]"
|
className="absolute top-[95dvh] left-0 w-full h-[calc(100%-100dvh)]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 className="text-[5.556vw] lg:px-[calc(7.153vw-1.667vw)] leading-[1] w-full font-light flex flex-col gap-[0.556vw] select-none">
|
<h2 className="text-[5.556vw] lg:px-[calc(7.153vw-1.667vw)] leading-[1] w-full font-light flex flex-col gap-[0.556vw] select-none">
|
||||||
@@ -95,6 +92,6 @@ export default function Premiere() {
|
|||||||
<span className="lg:title-m text-[#262626]/25 ">высота потолков</span>
|
<span className="lg:title-m text-[#262626]/25 ">высота потолков</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import Button from "../ui/Buttons/Button";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
|
export default function ResidentialForm() {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
id="premiere"
|
||||||
|
headerColorScheme="Dark"
|
||||||
|
className={"h-screen flex !p-0"}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/img/residential/1.png"
|
||||||
|
alt="Residential Form"
|
||||||
|
className="h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 flex flex-col justify-center items-center">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="/img/residential/2.png"
|
||||||
|
alt="Residential Form"
|
||||||
|
className="lg:w-[15vw] lg:mb-[2.5vw] object-cover"
|
||||||
|
/>
|
||||||
|
<p className="title-m lg:mb-[1.111vw]">
|
||||||
|
Хотите стать резидентом <br /> клубного дома у воды?
|
||||||
|
</p>
|
||||||
|
<Button className="" onClick={() => {}} type="Primary" size="S">
|
||||||
|
Оставить заявку
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import BookSlider from "../ui/BookSlider";
|
||||||
|
import Section from "../ui/Section";
|
||||||
|
|
||||||
|
export default function Residents() {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
id="residents"
|
||||||
|
headerColorScheme="Light"
|
||||||
|
className="bg-[url('/img/bg/building.png')] bg-cover bg-center"
|
||||||
|
>
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:w-[55.556vw] lg:h-[33.889vw]">
|
||||||
|
<BookSlider>
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">
|
||||||
|
{"(3) Пространства резидентов"}
|
||||||
|
</div>
|
||||||
|
<div className="text-black block text-m mt-[24.131vw] ">
|
||||||
|
Приватный фитнес-клуб с панорамным видом и премиальным оснащением.
|
||||||
|
Интеллектуальное пространство для тех, кто привык быть в форме и
|
||||||
|
ценит абсолютную приватность каждой тренировки.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage3.png')] bg-cover bg-center" />
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">
|
||||||
|
{"(3) Пространства резидентов"}
|
||||||
|
</div>
|
||||||
|
<div className="text-black block text-m mt-[24.131vw] ">
|
||||||
|
Приватный фитнес-клуб с панорамным видом и премиальным оснащением.
|
||||||
|
Интеллектуальное пространство для тех, кто привык быть в форме и
|
||||||
|
ценит абсолютную приватность каждой тренировки.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage3.png')] bg-cover bg-center" />
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">
|
||||||
|
{"(3) Пространства резидентов"}
|
||||||
|
</div>
|
||||||
|
<div className="text-black block text-m mt-[24.131vw] ">
|
||||||
|
Приватный фитнес-клуб с панорамным видом и премиальным оснащением.
|
||||||
|
Интеллектуальное пространство для тех, кто привык быть в форме и
|
||||||
|
ценит абсолютную приватность каждой тренировки.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage3.png')] bg-cover bg-center" />
|
||||||
|
<div className="bg-white p-[1.111vw] flex flex-col">
|
||||||
|
<div className="text-black caption-s">
|
||||||
|
{"(3) Пространства резидентов"}
|
||||||
|
</div>
|
||||||
|
<div className="text-black block text-m mt-[24.131vw] ">
|
||||||
|
Приватный фитнес-клуб с панорамным видом и премиальным оснащением.
|
||||||
|
Интеллектуальное пространство для тех, кто привык быть в форме и
|
||||||
|
ценит абсолютную приватность каждой тренировки.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[url('/img/mocks/bookPage3.png')] bg-cover bg-center" />
|
||||||
|
</BookSlider>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Arrow from "@/components/icons/Arrow";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
|
||||||
import HTMLFlipBook from "react-pageflip";
|
import HTMLFlipBook from "react-pageflip";
|
||||||
|
import CursorButtonWrapper from "./CursorButtonWrapper";
|
||||||
|
|
||||||
const FLIPPING_TIME = 500;
|
const FLIPPING_TIME = 500;
|
||||||
|
|
||||||
@@ -12,33 +10,11 @@ export default function BookSlider({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
|
|
||||||
const [cursorOnRightSide, setCursorOnRightSide] = useState(false);
|
|
||||||
const [bookWidth, setBookWidth] = useState(0);
|
const [bookWidth, setBookWidth] = useState(0);
|
||||||
const [bookHeight, setBookHeight] = useState(0);
|
const [bookHeight, setBookHeight] = useState(0);
|
||||||
const flipBookRef = useRef(null);
|
const flipBookRef = useRef(null);
|
||||||
const sliderRef = useRef<HTMLDivElement>(null);
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
const rectRef = useRef<DOMRect | null>(null);
|
|
||||||
const frameRef = useRef<number | null>(null);
|
|
||||||
const isFlipping = useRef(false);
|
const isFlipping = useRef(false);
|
||||||
const lastMouseEvent = useRef<MouseEvent | null>(null);
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
setIsHovered(true);
|
|
||||||
if (sliderRef.current) {
|
|
||||||
rectRef.current = sliderRef.current.getBoundingClientRect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setIsHovered(false);
|
|
||||||
|
|
||||||
if (frameRef.current) {
|
|
||||||
cancelAnimationFrame(frameRef.current);
|
|
||||||
frameRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sliderRef.current) {
|
if (sliderRef.current) {
|
||||||
@@ -47,56 +23,13 @@ export default function BookSlider({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const runFlip = (direction: "next" | "prev") => {
|
||||||
const handleMove = (e: MouseEvent) => {
|
|
||||||
lastMouseEvent.current = e;
|
|
||||||
|
|
||||||
if (!rectRef.current) return;
|
|
||||||
|
|
||||||
const x = e.clientX - rectRef.current.left;
|
|
||||||
const y = e.clientY - rectRef.current.top;
|
|
||||||
|
|
||||||
const inside =
|
|
||||||
e.clientX >= rectRef.current.left &&
|
|
||||||
e.clientX <= rectRef.current.right &&
|
|
||||||
e.clientY >= rectRef.current.top &&
|
|
||||||
e.clientY <= rectRef.current.bottom;
|
|
||||||
|
|
||||||
if (!inside) {
|
|
||||||
setIsHovered(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsHovered(true);
|
|
||||||
|
|
||||||
if (frameRef.current) return;
|
|
||||||
frameRef.current = requestAnimationFrame(() => {
|
|
||||||
setCursorPosition({ x, y });
|
|
||||||
setCursorOnRightSide(x > rectRef.current!.width / 2);
|
|
||||||
frameRef.current = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!sliderRef.current || !lastMouseEvent.current) return;
|
|
||||||
rectRef.current = sliderRef.current.getBoundingClientRect();
|
|
||||||
handleMove(lastMouseEvent.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("mousemove", handleMove);
|
|
||||||
window.addEventListener("scroll", handleScroll, true);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", handleMove);
|
|
||||||
window.removeEventListener("scroll", handleScroll, true);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
if (flipBookRef.current && !isFlipping.current) {
|
if (flipBookRef.current && !isFlipping.current) {
|
||||||
isFlipping.current = true;
|
isFlipping.current = true;
|
||||||
if (cursorOnRightSide) (flipBookRef.current as any).pageFlip().flipNext();
|
|
||||||
|
if (direction === "next") (flipBookRef.current as any).pageFlip().flipNext();
|
||||||
else (flipBookRef.current as any).pageFlip().flipPrev();
|
else (flipBookRef.current as any).pageFlip().flipPrev();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isFlipping.current = false;
|
isFlipping.current = false;
|
||||||
}, FLIPPING_TIME);
|
}, FLIPPING_TIME);
|
||||||
@@ -104,23 +37,12 @@ export default function BookSlider({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<CursorButtonWrapper
|
||||||
ref={sliderRef}
|
wrapperRef={sliderRef}
|
||||||
className="w-full relative h-full hover:cursor-none"
|
className="w-full h-full"
|
||||||
onMouseEnter={handleMouseEnter}
|
onRightClick={() => runFlip("next")}
|
||||||
onMouseLeave={handleMouseLeave}
|
onLeftClick={() => runFlip("prev")}
|
||||||
onClick={handleClick}
|
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{isHovered && (
|
|
||||||
<CursorButton
|
|
||||||
x={cursorPosition.x}
|
|
||||||
y={cursorPosition.y}
|
|
||||||
rightSide={cursorOnRightSide}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* @ts-expect-error Почему то хочет чтобы были вообще все пропсы (даже необязательные), хотя работает только и без них */}
|
{/* @ts-expect-error Почему то хочет чтобы были вообще все пропсы (даже необязательные), хотя работает только и без них */}
|
||||||
<HTMLFlipBook
|
<HTMLFlipBook
|
||||||
key={`${bookWidth}-${bookHeight}`}
|
key={`${bookWidth}-${bookHeight}`}
|
||||||
@@ -136,42 +58,6 @@ export default function BookSlider({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</HTMLFlipBook>
|
</HTMLFlipBook>
|
||||||
</div>
|
</CursorButtonWrapper>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CursorButton({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
rightSide,
|
|
||||||
}: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
rightSide: boolean;
|
|
||||||
}) {
|
|
||||||
const [width, setWidth] = useState(0);
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
ref={(el) => {
|
|
||||||
if (el) setWidth(el.getBoundingClientRect().width);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
transform: `translate3d(${x - width / 2}px, ${y - width / 2}px, 0)`,
|
|
||||||
}}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="absolute z-10 lg:size-[6.944vw] border border-[#FFFFFF]/25 backdrop-blur-[16px] flex items-center justify-center rounded-full bg-[#262626]/25 pointer-events-none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"lg:size-[1.736vw] transition-transform duration-300 ease-out",
|
|
||||||
rightSide ? "rotate-180" : ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Arrow direction="left" />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function Button({
|
|||||||
type === "Primary" &&
|
type === "Primary" &&
|
||||||
(disabled
|
(disabled
|
||||||
? "bg-[#262626]/6 text-[#262626]/6 backdrop-blur-[16px]"
|
? "bg-[#262626]/6 text-[#262626]/6 backdrop-blur-[16px]"
|
||||||
: "bg-[#262626]/6 text-[#262626] hover:bg-[#262626]/25 active:bg-[#262626] active:text-white"),
|
: "bg-[#262626]/6 text-[#262626] hover:bg-[#262626]/25 active:bg-[#262626] active:text-white bacdrop-blur-[16px]"),
|
||||||
type === "PrimaryInverse" &&
|
type === "PrimaryInverse" &&
|
||||||
(disabled
|
(disabled
|
||||||
? "bg-[#FFFFFF]/20 text-[#262626]/6 backdrop-blur-[16px]"
|
? "bg-[#FFFFFF]/20 text-[#262626]/6 backdrop-blur-[16px]"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function ButtonRound({
|
|||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<p className="button-s">{text}</p>
|
<p className="button-s whitespace-pre-wrap text-start">{text}</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import Arrow from "../icons/Arrow";
|
||||||
|
|
||||||
|
type CursorButtonWrapperProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
showCursor?: boolean;
|
||||||
|
onWrapperClick?: (rightSide: boolean) => void;
|
||||||
|
onRightClick?: () => void;
|
||||||
|
onLeftClick?: () => void;
|
||||||
|
onHoverChange?: (hovered: boolean) => void;
|
||||||
|
wrapperRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
|
renderCursor?: (params: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
rightSide: boolean;
|
||||||
|
}) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CursorButtonWrapper({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCursor = true,
|
||||||
|
onWrapperClick,
|
||||||
|
onRightClick,
|
||||||
|
onLeftClick,
|
||||||
|
onHoverChange,
|
||||||
|
wrapperRef,
|
||||||
|
renderCursor,
|
||||||
|
}: CursorButtonWrapperProps) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [cursorOnRightSide, setCursorOnRightSide] = useState(false);
|
||||||
|
const localWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const rectRef = useRef<DOMRect | null>(null);
|
||||||
|
const frameRef = useRef<number | null>(null);
|
||||||
|
const lastMouseEvent = useRef<MouseEvent | null>(null);
|
||||||
|
|
||||||
|
const targetRef = wrapperRef ?? localWrapperRef;
|
||||||
|
|
||||||
|
const setHovered = useCallback((value: boolean) => {
|
||||||
|
setIsHovered(value);
|
||||||
|
onHoverChange?.(value);
|
||||||
|
}, [onHoverChange]);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
setHovered(true);
|
||||||
|
if (targetRef.current) {
|
||||||
|
rectRef.current = targetRef.current.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
setHovered(false);
|
||||||
|
if (frameRef.current) {
|
||||||
|
cancelAnimationFrame(frameRef.current);
|
||||||
|
frameRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMove = (e: MouseEvent) => {
|
||||||
|
lastMouseEvent.current = e;
|
||||||
|
if (!rectRef.current) return;
|
||||||
|
|
||||||
|
const x = e.clientX - rectRef.current.left;
|
||||||
|
const y = e.clientY - rectRef.current.top;
|
||||||
|
|
||||||
|
const inside =
|
||||||
|
e.clientX >= rectRef.current.left &&
|
||||||
|
e.clientX <= rectRef.current.right &&
|
||||||
|
e.clientY >= rectRef.current.top &&
|
||||||
|
e.clientY <= rectRef.current.bottom;
|
||||||
|
|
||||||
|
if (!inside) {
|
||||||
|
setHovered(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHovered(true);
|
||||||
|
|
||||||
|
if (frameRef.current) return;
|
||||||
|
frameRef.current = requestAnimationFrame(() => {
|
||||||
|
if (!rectRef.current) return;
|
||||||
|
const rightSide = x > rectRef.current.width / 2;
|
||||||
|
setCursorPosition({ x, y });
|
||||||
|
setCursorOnRightSide(rightSide);
|
||||||
|
frameRef.current = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!targetRef.current || !lastMouseEvent.current) return;
|
||||||
|
rectRef.current = targetRef.current.getBoundingClientRect();
|
||||||
|
handleMove(lastMouseEvent.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMove);
|
||||||
|
window.addEventListener("scroll", handleScroll, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMove);
|
||||||
|
window.removeEventListener("scroll", handleScroll, true);
|
||||||
|
};
|
||||||
|
}, [setHovered, targetRef]);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (cursorOnRightSide) {
|
||||||
|
onRightClick?.();
|
||||||
|
} else {
|
||||||
|
onLeftClick?.();
|
||||||
|
}
|
||||||
|
onWrapperClick?.(cursorOnRightSide);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={targetRef}
|
||||||
|
className={clsx("relative hover:cursor-none", className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{showCursor &&
|
||||||
|
isHovered &&
|
||||||
|
(renderCursor ? (
|
||||||
|
renderCursor({
|
||||||
|
x: cursorPosition.x,
|
||||||
|
y: cursorPosition.y,
|
||||||
|
rightSide: cursorOnRightSide,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<CursorButton
|
||||||
|
x={cursorPosition.x}
|
||||||
|
y={cursorPosition.y}
|
||||||
|
rightSide={cursorOnRightSide}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CursorButton({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
rightSide,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
rightSide: boolean;
|
||||||
|
}) {
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) setWidth(el.getBoundingClientRect().width);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
transform: `translate3d(${x - width / 2}px, ${y - width / 2}px, 0)`,
|
||||||
|
}}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="absolute z-10 lg:size-[6.944vw] border border-[#FFFFFF]/25 backdrop-blur-[16px] flex items-center justify-center rounded-full bg-[#262626]/25 pointer-events-none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"lg:size-[1.736vw] transition-transform duration-300 ease-out text-white",
|
||||||
|
rightSide ? "rotate-180" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Arrow direction="left" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full lg:h-[0.069vw] bg-white",
|
"w-full lg:h-[0.069vw] ",
|
||||||
headerColorScheme === "Light" || menuOpen
|
headerColorScheme === "Light" || menuOpen
|
||||||
? "bg-white"
|
? "bg-white"
|
||||||
: "bg-[#262626]"
|
: "bg-[#262626]"
|
||||||
@@ -42,7 +42,7 @@ export default function Header() {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full lg:h-[0.069vw] bg-white",
|
"w-full lg:h-[0.069vw] ",
|
||||||
headerColorScheme === "Light" || menuOpen
|
headerColorScheme === "Light" || menuOpen
|
||||||
? "bg-white"
|
? "bg-white"
|
||||||
: "bg-[#262626]"
|
: "bg-[#262626]"
|
||||||
@@ -50,7 +50,7 @@ export default function Header() {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full lg:h-[0.069vw] bg-white",
|
"w-full lg:h-[0.069vw] ",
|
||||||
headerColorScheme === "Light" || menuOpen
|
headerColorScheme === "Light" || menuOpen
|
||||||
? "bg-white"
|
? "bg-white"
|
||||||
: "bg-[#262626]"
|
: "bg-[#262626]"
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import Add from "../icons/Add";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
|
||||||
|
export default function Hint({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
direction = "left",
|
||||||
|
openByDefault = false,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
direction?: "left" | "right";
|
||||||
|
openByDefault?: boolean;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(openByDefault);
|
||||||
|
const [buttonSize, setButtonSize] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) {
|
||||||
|
setButtonSize(el.getBoundingClientRect().width);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="w-full aspect-square rounded-full bg-[#FFFFFF]/40 backdrop-blur-[16px] relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"w-[55%] aspect-square rounded-full bg-white hover:opacity-90 text-[#262626]/60 p-[10%] transition-transform duration-300 z-[2]",
|
||||||
|
open && "rotate-45"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
</div>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{
|
||||||
|
left:
|
||||||
|
direction === "left" ? `${(buttonSize || 0) / 2}px` : "auto",
|
||||||
|
right:
|
||||||
|
direction === "right" ? `${(buttonSize || 0) / 2}px` : "auto",
|
||||||
|
bottom: `${(buttonSize || 0) / 2}px`,
|
||||||
|
}}
|
||||||
|
className="lg:p-[1.111vw] lg:rounded-[0.278vw] bg-[#DBDBDB] absolute right-0 bottom-0 z-[1] text-start "
|
||||||
|
>
|
||||||
|
<p className="title-s whitespace-nowrap text-[#333333] mb-[0.556vw]">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[#333333]/60 whitespace-pre-line">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { useAppStateStore } from "@/stores/useAppStateStore";
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
id: string;
|
||||||
|
ref?: React.RefObject<HTMLElement | null>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
headerColorScheme: "Light" | "Dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Section({
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
headerColorScheme,
|
||||||
|
}: SectionProps) {
|
||||||
|
const setHeaderColorScheme = useAppStateStore(
|
||||||
|
(state) => state.setHeaderColorScheme
|
||||||
|
);
|
||||||
|
const anchorRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const baseClassName = `
|
||||||
|
relative w-full min-h-screen text-black bg-white bg-cover bg-center
|
||||||
|
lg:px-[1.667vw] lg:pt-[7.778vw] lg:pb-[9.444vw]
|
||||||
|
`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = anchorRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHeaderColorScheme(headerColorScheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [setHeaderColorScheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={ref ?? null}
|
||||||
|
id={id}
|
||||||
|
className={clsx(baseClassName, className)}
|
||||||
|
>
|
||||||
|
{/* Когда блок будет находиться почти полностью на экране (top=95dvh) тогда элемент появится и хедер поменяется */}
|
||||||
|
<div
|
||||||
|
ref={anchorRef}
|
||||||
|
className="absolute top-[95dvh] left-0 w-full h-[calc(100%-100dvh)]"
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { VIDEO_SLIDER_CONTENT } from "@/data/videos";
|
||||||
|
import Arrow from "../icons/Arrow";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import NumberFlow from "@number-flow/react";
|
||||||
|
import RoundButton from "./Buttons/ButtonRound";
|
||||||
|
import Tour3D from "../icons/Tour3D";
|
||||||
|
|
||||||
|
export default function VideoPlayer({
|
||||||
|
className,
|
||||||
|
expanded,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
expanded?: boolean;
|
||||||
|
}) {
|
||||||
|
const videosCount = VIDEO_SLIDER_CONTENT.length;
|
||||||
|
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
setCurrentVideoIndex((prev) => (prev + 1) % videosCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
setCurrentVideoIndex((prev) => (prev - 1 + videosCount) % videosCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full relative">
|
||||||
|
<video
|
||||||
|
src={VIDEO_SLIDER_CONTENT[currentVideoIndex].video}
|
||||||
|
poster={VIDEO_SLIDER_CONTENT[currentVideoIndex].video}
|
||||||
|
playsInline
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute left-1/2 bottom-0 -translate-x-1/2 flex items-center justify-between lg:gap-[0.833vw]"
|
||||||
|
animate={{
|
||||||
|
width: expanded ? "65.125vw" : "35.833vw",
|
||||||
|
bottom: expanded ? "8.333vw" : "2.222vw",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Title */}
|
||||||
|
<motion.p
|
||||||
|
animate={{
|
||||||
|
fontSize: expanded ? "2.778vw" : "1.667vw",
|
||||||
|
}}
|
||||||
|
className="title-l font-light text-white whitespace-pre-wrap"
|
||||||
|
>
|
||||||
|
{VIDEO_SLIDER_CONTENT[currentVideoIndex].title}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<motion.div className="absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 flex items-center lg:gap-[0.833vw]">
|
||||||
|
<motion.button
|
||||||
|
onClick={handlePrevious}
|
||||||
|
animate={{
|
||||||
|
width: expanded ? "5vw" : "3.056vw",
|
||||||
|
}}
|
||||||
|
className="aspect-square bg-white hover:bg-white/90 active:bg-white/60 rounded-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
width: expanded ? "1.944vw" : "1.181vw",
|
||||||
|
}}
|
||||||
|
className="aspect-square"
|
||||||
|
>
|
||||||
|
<Arrow direction="left" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
onClick={handleNext}
|
||||||
|
animate={{
|
||||||
|
width: expanded ? "5vw" : "3.056vw",
|
||||||
|
}}
|
||||||
|
className="aspect-square bg-white hover:bg-white/90 active:bg-white/60 rounded-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
width: expanded ? "1.944vw" : "1.181vw",
|
||||||
|
}}
|
||||||
|
className="aspect-square"
|
||||||
|
>
|
||||||
|
<Arrow direction="right" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Index */}
|
||||||
|
<motion.p
|
||||||
|
animate={{
|
||||||
|
fontSize: expanded ? "2.778vw" : "1.667vw",
|
||||||
|
}}
|
||||||
|
className="text-m text-white font-light"
|
||||||
|
>
|
||||||
|
<NumberFlow value={currentVideoIndex + 1} /> / {videosCount}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<RoundButton
|
||||||
|
className="!absolute !-bottom-[5vw] !left-[2vw]"
|
||||||
|
type="White"
|
||||||
|
text={"Исследовать\nпространство"}
|
||||||
|
icon={<Tour3D />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { VideoPlayerContentItem } from "@/types";
|
||||||
|
|
||||||
|
export const VIDEO_SLIDER_CONTENT: VideoPlayerContentItem[] = [
|
||||||
|
{
|
||||||
|
title: "Прогулочный\n бульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Прогулочный\nбульвар",
|
||||||
|
video: "/video/mock.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
@@ -11,9 +11,23 @@ interface AppStateStore {
|
|||||||
|
|
||||||
export const useAppStateStore = create<AppStateStore>((set) => ({
|
export const useAppStateStore = create<AppStateStore>((set) => ({
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
setIsLoading: (isLoading) => set({ isLoading }),
|
setIsLoading: (isLoading) => {
|
||||||
|
set({ isLoading });
|
||||||
|
if (isLoading) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
}
|
||||||
|
},
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
setMenuOpen: (menuOpen) => set({ menuOpen }),
|
setMenuOpen: (menuOpen) => {
|
||||||
|
if (menuOpen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
}
|
||||||
|
set({ menuOpen });
|
||||||
|
},
|
||||||
headerColorScheme: "Light",
|
headerColorScheme: "Light",
|
||||||
setHeaderColorScheme: (headerColorScheme) => set({ headerColorScheme }),
|
setHeaderColorScheme: (headerColorScheme) => set({ headerColorScheme }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface VideoPlayerContentItem {
|
||||||
|
title: string;
|
||||||
|
video: string;
|
||||||
|
}
|
||||||
@@ -18,8 +18,15 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@yandex/ymaps3-types"
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"ymaps3": [
|
||||||
|
"./node_modules/@yandex/ymaps3-types"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||