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, " ")} ₽*/}
- - + setCoupon(e.target.value)}/> +
- {/*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 ( + <> +