diff --git a/next-sitemap.config.js b/next-sitemap.config.js
new file mode 100644
index 0000000..115844b
--- /dev/null
+++ b/next-sitemap.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next-sitemap').IConfig} */
+module.exports = {
+ siteUrl: 'https://relynolli.ru',
+ generateRobotsTxt: true, //
+ // ...other options
+}
\ No newline at end of file
diff --git a/next.config.mjs b/next.config.mjs
index 010fc5d..37967d5 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
- reactStrictMode: true,
+ reactStrictMode: false,
images: {
remotePatterns: [
{
@@ -17,32 +17,32 @@ const nextConfig = {
],
},
- webpack(config) {
- const fileLoaderRule = config.module.rules.find((rule) =>
- rule.test?.test?.('.svg'),
- )
+ webpack(config) {
+ const fileLoaderRule = config.module.rules.find((rule) =>
+ rule.test?.test?.('.svg'),
+ )
- config.module.rules.push(
- // Reapply the existing rule, but only for svg imports ending in ?url
- {
- ...fileLoaderRule,
- test: /\.svg$/i,
- resourceQuery: /url/, // *.svg?url
- },
- // Convert all other *.svg imports to React components
- {
- test: /\.svg$/i,
- issuer: fileLoaderRule.issuer,
- resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
- use: ['@svgr/webpack'],
- },
- )
+ config.module.rules.push(
+ // Reapply the existing rule, but only for svg imports ending in ?url
+ {
+ ...fileLoaderRule,
+ test: /\.svg$/i,
+ resourceQuery: /url/, // *.svg?url
+ },
+ // Convert all other *.svg imports to React components
+ {
+ test: /\.svg$/i,
+ issuer: fileLoaderRule.issuer,
+ resourceQuery: {not: [...fileLoaderRule.resourceQuery.not, /url/]}, // exclude if *.svg?url
+ use: ['@svgr/webpack'],
+ },
+ )
- // Modify the file loader rule to ignore *.svg, since we have it handled now.
- fileLoaderRule.exclude = /\.svg$/i
- return config
- }
+ // Modify the file loader rule to ignore *.svg, since we have it handled now.
+ fileLoaderRule.exclude = /\.svg$/
+ return config
+ }
};
export default nextConfig;
diff --git a/package.json b/package.json
index 39aa361..97dabf2 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,18 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "postbuild": "next-sitemap"
},
"dependencies": {
+ "@cdek-it/widget": "3",
"@nextui-org/react": "^2.2.9",
+ "@pbe/react-yandex-maps": "^1.2.5",
"@tanstack/react-query": "^5.20.5",
"@tanstack/react-query-devtools": "^5.21.7",
+ "@types/lodash": "^4.17.0",
+ "@types/yandex-maps": "^2.1.35",
+ "@uidotdev/usehooks": "^2.4.1",
"axios": "^1.6.7",
"framer-motion": "^11.0.3",
"libphonenumber-js": "^1.10.56",
@@ -19,11 +25,14 @@
"mysql2": "^3.9.1",
"nanoid": "^5.0.5",
"next": "14.1.0",
+ "next-sitemap": "^4.2.3",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
"react-image": "^4.1.0",
"react-imask": "^7.5.0",
+ "react-stately": "^3.30.1",
+ "react-ymaps3": "^0.0.19",
"swiper": "^11.0.7",
"valtio": "^1.13.0",
"zod": "^3.22.4"
@@ -34,6 +43,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "@yandex/ymaps3-types": "^0.0.24",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
diff --git a/public/YouTubeIcon.svg b/public/YouTubeIcon.svg
new file mode 100644
index 0000000..e6b4359
--- /dev/null
+++ b/public/YouTubeIcon.svg
@@ -0,0 +1,12 @@
+
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..6ecde44
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,9 @@
+# *
+User-agent: *
+Allow: /
+
+# Host
+Host: https://relynolli.ru
+
+# Sitemaps
+Sitemap: https://relynolli.ru/sitemap.xml
diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml
new file mode 100644
index 0000000..bbc291b
--- /dev/null
+++ b/public/sitemap-0.xml
@@ -0,0 +1,36 @@
+
+
+https://relynolli.ru2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/cart2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/contact2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/order/make2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-lh-10w-40-sn-cf-1l-kanistra-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-lh-10w-40-sn-cf-4l-kanistra-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-30-sn-cf-1l-kanistra-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-30-sn-cf-205l-bochka-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-30-sn-cf-4l-kanistra-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-40-sn-cf-1l-kanistra-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-40-sn-cf-205l-bochka-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-40-sn-cf-4l-kanistra-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-standart-m1-lh-10w-40-slcf-1l-kanistra-acea-a3b42024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/catalog/maslo-motornoe-relynolli-standart-m1-lh-10w-40-slcf-4l-kanistra-acea-a3b42024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles/poyasnenie-po-sootvetstviyu-masel-relynolli-klassu-acea-a5-b52024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles/professionalnoe-retsenzirovanie-dissertatsii-ot-ntts-td-tekhnokhim-grupp2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles/tsifrovizatsiya-sklada-otgruzki-bez-oshibok-relynolli2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles/realnye-ispytaniya-masla-ili-fokus-pokus2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles/nauchno-tekhnicheskiy-tsentr-vozmozhnosti-i-perspektivy2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/articles/skhema-ispytaniy-relynolli-garantiya-nadezhnosti-i-dostovernosti2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/meniaj_besplatno2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/tv-reportazh-relynolli2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/motornoe-maslo-relynolli-v-podarok2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/samarskij-politeh-relynolli2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/start-sotrudnichestva-service-trans-cargo-tehnohim-relynolli2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/tehnohim-yarmarka-vakanciy2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/skidka-10-dlya-podpischikov-kanala-telegram2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/kazautoexpo-2023-relynolli2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/relynolli-magazin-roznica2024-04-15T14:24:16.500Zdaily0.7
+https://relynolli.ru/news/bisness-yaroslavii2024-04-15T14:24:16.500Zdaily0.7
+
\ No newline at end of file
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..da9358d
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,4 @@
+
+
+https://relynolli.ru/sitemap-0.xml
+
\ No newline at end of file
diff --git a/src/components/pages/cart/orderInfo.tsx b/src/components/pages/cart/orderInfo.tsx
index b4aec2c..215f9ed 100644
--- a/src/components/pages/cart/orderInfo.tsx
+++ b/src/components/pages/cart/orderInfo.tsx
@@ -3,18 +3,28 @@ import {Button, Checkbox} from "@nextui-org/react";
import {ChevronRightIcon} from "@nextui-org/shared-icons";
import {useQuery} from "@tanstack/react-query";
import LocalAPI from "@/service/localAPI";
-
+import {useState} from "react";
+import _ from 'lodash';
+import Link from "next/link";
type OrderInfoProps = {
setIsDisabled: (value: boolean) => void
isDisabled: boolean
}
const OrderInfo = (props: OrderInfoProps) => {
+
+ const [coupon, setCoupon] = useState("");
+ const [couponApplied, setCouponApplied] = useState(false)
+
+
const totalProductPriceQs = useQuery(
{
- queryKey: ['totalProductPrice'], queryFn: async () => {
+ queryKey: ['totalProductPrice', couponApplied, coupon], queryFn: async () => {
const service = new LocalAPI()
- return await service.totalProductPrice()
+ if (couponApplied) {
+ return await service.totalProductPrice(coupon)
+ }
+ return await service.totalProductPrice(undefined)
}
}
)
@@ -23,20 +33,19 @@ const OrderInfo = (props: OrderInfoProps) => {
Информация о заказе
Товаров на:
- {String(totalProductPriceQs.data ? totalProductPriceQs.data.data!.total : 0).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽
+ {/* TODO round value */}
+ {/*{String(totalProductPriceQs.data ? _.sum(totalProductPriceQs.data.data!.items.map(item => item.cart.product.price.BASE * item.cart.quantity)) : 0).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽*/}
- {/*TODO calculate discount*/}
-
Скидка:
- 0 ₽
+ {/*{totalProductPriceQs.data ? _.sum(totalProductPriceQs.data.data!.items.map(item => item.cart.product.price.BASE * item.cart.quantity)) - totalProductPriceQs.data.data!.total : 0} ₽*/}
{/*TODO calculate discount + shipment*/}
@@ -55,7 +64,7 @@ const OrderInfo = (props: OrderInfoProps) => {
props.setIsDisabled(!props.isDisabled)
}}
className={"[&_span:last-child]:!text-[#808080] [&_span:last-child]:text-subtitle-5 [&_span:last-child]:leading-normal mb-6"}>Нажимая
- кнопку «Оформить заказ», я даю согласие на обработку моих персональных данных
+ кнопку «Оформить заказ», я даю согласие на обработку моих персональных данных
)
diff --git a/src/components/pages/order/addressInput.tsx b/src/components/pages/order/addressInput.tsx
new file mode 100644
index 0000000..83fe330
--- /dev/null
+++ b/src/components/pages/order/addressInput.tsx
@@ -0,0 +1,60 @@
+import {Autocomplete, AutocompleteItem, cn} from "@nextui-org/react";
+import {useDebounce} from "@uidotdev/usehooks";
+import localAPI from "@/service/localAPI";
+import {Controller} from "react-hook-form";
+import {InputPropsType} from "@/components/pages/order/types";
+import {useQuery} from "@tanstack/react-query";
+import _ from "lodash";
+
+const AddressInput = (props: InputPropsType) => {
+ const address = props.watch("address")
+ const debouncedAddress = useDebounce(address, 500)
+
+ const addressInfo = useQuery({
+ queryKey: ["address_info", debouncedAddress],
+ queryFn: async () => {
+ const service = new localAPI()
+ return await service.fetchAddresses(debouncedAddress)
+ },
+ staleTime: 1000
+ })
+
+
+ return (
+
+ {
+ props.setValue("address", value)
+ const obj = _.find(addressInfo.data, (p1) => p1.metaDataProperty.GeocoderMetaData.Address.formatted === value)
+ if (typeof obj !== "undefined") {
+ props.setValue("lat", +(obj.Point.pos.split(' ')[1]))
+ props.setValue("lon", +( obj.Point.pos.split(' ')[0]))
+ }
+ }}
+ items={addressInfo.data || []}
+ >
+ { item => (
+
+ {item.metaDataProperty.GeocoderMetaData.Address.formatted}
+
+ )}
+
+ } name={"address"} control={props.control} rules={{required: "Поле обязательно для заполнения"}} />
+ )
+}
+
+
+export default AddressInput
\ No newline at end of file
diff --git a/src/components/pages/order/cdekMap/index.tsx b/src/components/pages/order/cdekMap/index.tsx
new file mode 100644
index 0000000..a33f10f
--- /dev/null
+++ b/src/components/pages/order/cdekMap/index.tsx
@@ -0,0 +1,165 @@
+import {Dispatch, SetStateAction, useEffect, useRef, useState} from "react"
+import Script from "next/script";
+import {YMap, type YMapLocationRequest} from 'ymaps3';
+import {Spinner, Tooltip} from "@nextui-org/react";
+import {InputPropsType} from "@/components/pages/order/types";
+import {useQuery} from "@tanstack/react-query";
+import LocalAPI from "@/service/localAPI";
+import {SdekPoint} from "@/service/types/local";
+
+
+type GeoObject = {
+ lat: number,
+ lon: number,
+ name: string,
+ description: string
+ pvzId: string
+}
+
+type SdekMapProps = {
+ objects: GeoObject[],
+ center: [number, number],
+ zoom?: number,
+} & Omit
+
+const initMap = async (ref: React.RefObject,
+ setMapIsLoaded: Dispatch>,
+ setMap: Dispatch>
+) => {
+
+ await ymaps3.ready;
+ setMapIsLoaded(true)
+
+ const LOCATION: YMapLocationRequest = {
+ center: [37.623082, 55.75254],
+ zoom: 9
+ };
+
+ const {YMap, YMapDefaultSchemeLayer, YMapMarker} = ymaps3;
+ if (!ref.current) return
+
+ const map = new YMap(ref.current, {location: LOCATION});
+
+ map.addChild(new YMapDefaultSchemeLayer({}))
+ .addChild(new ymaps3.YMapDefaultFeaturesLayer({zIndex: 1800}))
+ setMap(map)
+
+
+}
+
+
+const SdekElem = ({map, object, watch, setValue}: { map: YMap | null, object: SdekPoint } & Omit) => {
+ const ref = useRef(null);
+ const selectedPvzId = watch("pvzId")
+
+
+ useEffect(() => {
+ if (!map || !ref.current) return
+ const marker = new ymaps3.YMapMarker({
+ coordinates: [object.location.longitude, object.location.latitude],
+ draggable: false,
+ }, ref.current)
+
+ map.addChild(marker)
+
+ }, [map, ref.current]);
+
+ return (
+
+ {object.name}
+ {object.note}
+
+ }>
+ setValue("pvzId", object.code)}>
+
+
+
+
+ )
+}
+
+const CDEKLogo = ({isSelected}: { isSelected: boolean }) => (
+
+
+
+)
+
+const CdekMap = (props: SdekMapProps) => {
+ const ref = useRef(null);
+ const [mapIsLoaded, setMapIsLoaded] = useState(false)
+ const [mapInstance, setMapInstance] = useState(null)
+
+ const lat = props.watch("lat")
+ const lon = props.watch("lon")
+
+ const sdekPoints = useQuery({
+ queryKey: ["sdek-points", lat, lon],
+ queryFn: async () => {
+ const service = new LocalAPI()
+ if (!lat || !lon){ return []}
+
+ return await service.fetchSdekPoints(lat, lon)
+ }
+ })
+
+ useEffect(() => {
+ if (!mapInstance || !mapIsLoaded) return
+ if (lat && lon) {
+ const LOCATION: YMapLocationRequest = {
+ center: [lon, lat],
+ zoom: 15
+ };
+ mapInstance.setLocation(LOCATION)
+ }
+
+ }, [mapInstance, mapIsLoaded, lat, lon]);
+ return (
+ <>
+