diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..a645760f13d105ed3ec83fb9b41afa3c7919dd24 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PUBLIC_URL = 'http://localhost:5173' +API_URL = 'http://localhost:3001/api/' diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..f5b687cb01933c0134e7029151d68b7356c69127 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PUBLIC_URL = 'http://localhost:PORT' +API_URL = 'http://localhost:PORT/api/' \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 172aa2e80fc1f4e531ca28a3c288cd30d00a1e60..dd0a012b9263e92eb20649e67a4815806d0e394b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,15 +8,15 @@ // EXTENDS // "extends": [ - "next/core-web-vitals", "airbnb", "airbnb/hooks", - "plugin:react/recommended", - "plugin:react-hooks/recommended", "plugin:jsx-a11y/recommended", + "plugin:react-hooks/recommended", + "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "eslint:recommended", - "next" + "next/core-web-vitals", + "plugin:prettier/recommended" ], // // PLUGINS @@ -26,7 +26,6 @@ "react-hooks", "jsx-a11y", "@typescript-eslint/eslint-plugin" - // "prettier" ], // // PARSER @@ -90,7 +89,7 @@ // https://eslint.org/docs/latest/rules/no-underscore-dangle "no-underscore-dangle": 0, // https://eslint.org/docs/latest/rules/quotes - "quotes": [1, "single"], + "quotes": [1, "single", { "avoidEscape": true }], // https://eslint.org/docs/latest/rules/jsx-quotes "jsx-quotes": [1, "prefer-double"], // https://eslint.org/docs/latest/rules/comma-dangle @@ -139,7 +138,7 @@ ], // https://eslint.org/docs/latest/rules/object-curly-newline "object-curly-newline": [ - 1, + 0, { "ObjectExpression": { "minProperties": 4, @@ -205,7 +204,7 @@ "import/newline-after-import": [ 1, { - "count": 2 + "count": 1 } ], // https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/max-dependencies.md @@ -293,7 +292,7 @@ ], // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-indent.md "react/jsx-indent": [ - 1, + 0, 2, { "checkAttributes": true, @@ -330,7 +329,7 @@ ], // https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-curly-spacing.md "react/jsx-curly-spacing": [ - 1, + 0, { "allowMultiline": false, "children": { @@ -339,7 +338,7 @@ "spacing": { "objectLiterals": "never" }, - "when": "always" + "when": "never" } ], // https://typescript-eslint.io/rules/adjacent-overload-signatures/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..fcac57607cf23e7f95877769f97d049db466e8d2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +build +coverage + +# Ignore all HTML files: +**/*.html \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..cdb0f9e2bba72bf4f41c0e590dd1195666ad75cc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "jsxSingleQuote": false, + "endOfLine": "lf" + } + \ No newline at end of file diff --git a/.stylelintrc.json b/.stylelintrc.json index 4f5d805b026c695c6678b88dadc06ad9d7ac08f8..c8e240329a55dfe76ed931170beba80a38cef9fe 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,15 +1,14 @@ { - "plugins": [ - "stylelint-order" - ], + "customSyntax": "postcss-styled-syntax", "extends": [ "stylelint-config-standard", - "stylelint-config-styled-components", - "stylelint-config-recommended", - "stylelint-config-standard-scss", - "stylelint-config-recess-order" + "stylelint-config-recess-order", + "stylelint-prettier/recommended" ], - - "customSyntax": "postcss-styled-syntax" + + "rules": { + "media-query-no-invalid": null, + "no-empty-source": null + } } diff --git a/next.config.mjs b/next.config.mjs index ada18844cf67167910dab219bf423251514c2376..dee2c6f3226f3bc0b78791d544c589c40d68cefa 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - /* config options here */ compiler: { styledComponents: true, }, diff --git a/package.json b/package.json index 422f142c0ae4aa360b3294ad778af0368d345aea..659c7abaa6a478f0d3aea60234fef2fc7456b59d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "lint:fix": "yarn lint -- --fix", + "lint:fix": "yarn lint --fix", "stylelint": "stylelint ./src/**/*.tsx", "postinstall": "husky" }, @@ -34,24 +34,24 @@ "react-indiana-drag-scroll": "^3.0.3-alpha", "react-number-format": "^5.3.4", "react-redux": "^9.1.1", - "react-router-dom": "^6.22.3", "react-select": "^5.8.0", "redux": "^5.0.1", "redux-persist": "^6.0.0", "styled-components": "^6.1.8", + "stylelint-config-clean-order": "^5.4.2", "swiper": "^11.0.7", "zod": "^3.23.4" }, "devDependencies": { + "@next/eslint-plugin-next": "^14.2.3", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.1.1", - "@vitejs/plugin-react": "^4.2.1", "concurrently": "^8.2.2", - "eslint": "^8", + "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-next": "14.2.3", "eslint-config-prettier": "^9.1.0", @@ -64,23 +64,19 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "husky": "^9.0.11", - "lint-staged": "^15.2.5", + "postcss-styled-components": "^0.2.1", "postcss-styled-syntax": "^0.6.4", - "prettier": "^3.2.5", + "prettier": "3.2.5", "stylelint": "^16.6.1", - "stylelint-config-recess-order": "^5.0.0", + "stylelint-config-prettier": "^9.0.5", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-recess-order": "^5.0.1", "stylelint-config-recommended": "^14.0.0", "stylelint-config-standard": "^36.0.0", "stylelint-config-standard-scss": "^13.1.0", - "stylelint-config-styled-components": "^0.1.1", "stylelint-order": "^6.0.4", - "stylelint-processor-styled-components": "^1.10.0", + "stylelint-prettier": "^5.0.0", "stylelint-webpack-plugin": "^5.0.1", - "typescript": "^5", - "vite": "^5.1.6", - "vite-plugin-eslint": "^1.8.1" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": "yarn lint" + "typescript": "^5" } } diff --git a/public/coffee.jpg b/public/coffee.jpg new file mode 100644 index 0000000000000000000000000000000000000000..459659d6295ad567c32e4750097f2fa460131cb9 Binary files /dev/null and b/public/coffee.jpg differ diff --git a/public/dots.svg b/public/dots.svg new file mode 100644 index 0000000000000000000000000000000000000000..26017e79a375fceac5e8b98803d62d7f5c453ab5 --- /dev/null +++ b/public/dots.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/emoji-check.png b/public/emoji-check.png new file mode 100644 index 0000000000000000000000000000000000000000..0b3cf3851dcb0c7855d07a5a14e452cb3e5d84a7 Binary files /dev/null and b/public/emoji-check.png differ diff --git a/public/emoji-error.png b/public/emoji-error.png new file mode 100644 index 0000000000000000000000000000000000000000..e2630dc123c813dea0749763bc0ce33a88c11c07 Binary files /dev/null and b/public/emoji-error.png differ diff --git a/public/emoji-success.png b/public/emoji-success.png new file mode 100644 index 0000000000000000000000000000000000000000..a4cf2311259d32cb3ed9c0e9f431ca2b899448b3 Binary files /dev/null and b/public/emoji-success.png differ diff --git a/public/img-placeholder.png b/public/img-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..d3029c5e4b66955dfb324ccb81f9eb5b15508fe5 Binary files /dev/null and b/public/img-placeholder.png differ diff --git a/public/not-found-widget.png b/public/not-found-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..56102c8ceff41b8c5622eb748316e1ce1e251a38 Binary files /dev/null and b/public/not-found-widget.png differ diff --git a/public/patterns/clouds.png b/public/patterns/clouds.png new file mode 100644 index 0000000000000000000000000000000000000000..7617df97979e729a57eeccbd2a11755803da70b1 Binary files /dev/null and b/public/patterns/clouds.png differ diff --git a/public/patterns/diamonds.png b/public/patterns/diamonds.png new file mode 100644 index 0000000000000000000000000000000000000000..e079fcdbf8cdb4f2c268248fddeb03cfd1d79110 Binary files /dev/null and b/public/patterns/diamonds.png differ diff --git a/public/patterns/hexagons.png b/public/patterns/hexagons.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc464897d97e2cd5dd831a076ba7ad72062b187 Binary files /dev/null and b/public/patterns/hexagons.png differ diff --git a/public/patterns/leaves.png b/public/patterns/leaves.png new file mode 100644 index 0000000000000000000000000000000000000000..6f80dd968d31742d8565a4d99c15543f64d9c3b8 Binary files /dev/null and b/public/patterns/leaves.png differ diff --git a/public/patterns/modal-waves.png b/public/patterns/modal-waves.png new file mode 100644 index 0000000000000000000000000000000000000000..7641c620074fe914bff536476d23cfc8a036f7f7 Binary files /dev/null and b/public/patterns/modal-waves.png differ diff --git a/public/patterns/morphing.png b/public/patterns/morphing.png new file mode 100644 index 0000000000000000000000000000000000000000..f045b1bc562a7ac1273c58e4521c722e6138f6a2 Binary files /dev/null and b/public/patterns/morphing.png differ diff --git a/public/patterns/noise.png b/public/patterns/noise.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb2a73058ef0276b07893cf9a3fef8a6ff393fd Binary files /dev/null and b/public/patterns/noise.png differ diff --git a/public/patterns/square.png b/public/patterns/square.png new file mode 100644 index 0000000000000000000000000000000000000000..86df49ede28878be1f56ed2d6b6a1692b94452e0 Binary files /dev/null and b/public/patterns/square.png differ diff --git a/public/payment-border.svg b/public/payment-border.svg new file mode 100644 index 0000000000000000000000000000000000000000..713f13a6d91757e44d23e01a4317fa9531c0e500 --- /dev/null +++ b/public/payment-border.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/payment-mask.svg b/public/payment-mask.svg new file mode 100644 index 0000000000000000000000000000000000000000..69a828697427300b3b68ea5bc54d2c82d90a1b5b --- /dev/null +++ b/public/payment-mask.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/product-coffee.png b/public/product-coffee.png new file mode 100644 index 0000000000000000000000000000000000000000..511ae9732e9e0fb8157dabb65d210af53294c48a Binary files /dev/null and b/public/product-coffee.png differ diff --git a/public/server-error-widget.png b/public/server-error-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..6121331d62aa7cc213375a00b99c3f07eb983931 Binary files /dev/null and b/public/server-error-widget.png differ diff --git a/public/sprite-avatar.svg b/public/sprite-avatar.svg new file mode 100644 index 0000000000000000000000000000000000000000..e125164db9992df7b4a8294627ee99650c9dfffa --- /dev/null +++ b/public/sprite-avatar.svg @@ -0,0 +1,1075 @@ + \ No newline at end of file diff --git a/public/sprite-logo.svg b/public/sprite-logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..740c9e2a6a454fbac545398325e85639353b87be --- /dev/null +++ b/public/sprite-logo.svg @@ -0,0 +1,147 @@ + diff --git a/src/api-hooks/index.ts b/src/api-hooks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1c0a2dcbcc9335b4e4bf553fa5850a6355d12b7 --- /dev/null +++ b/src/api-hooks/index.ts @@ -0,0 +1,10 @@ +export * from './use-catalog'; +export * from './use-category'; +export * from './use-product'; +export * from './use-shops'; +export * from './use-recommendations'; +export * from './use-payment'; +export * from './use-user'; +export * from './use-logout'; +export * from './use-login'; +export * from './use-register'; diff --git a/src/api-hooks/use-catalog/index.ts b/src/api-hooks/use-catalog/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..50c3f55e3a37a2401faaedb621807e8603a400fa --- /dev/null +++ b/src/api-hooks/use-catalog/index.ts @@ -0,0 +1 @@ +export * from './use-catalog'; diff --git a/src/api-hooks/use-catalog/use-catalog.ts b/src/api-hooks/use-catalog/use-catalog.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ae2fac0a0ec1bdee064411106f10d802556092b --- /dev/null +++ b/src/api-hooks/use-catalog/use-catalog.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getCatalog } from '@/api'; + +export const useCatalog = (shop: string) => + useQuery({ + queryKey: ['catalog', shop], + queryFn: async () => getCatalog(shop), + }); diff --git a/src/api-hooks/use-category/index.ts b/src/api-hooks/use-category/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..493dde2eba4e13ec96c55752ef0f8a4207730806 --- /dev/null +++ b/src/api-hooks/use-category/index.ts @@ -0,0 +1 @@ +export * from './use-category'; diff --git a/src/api-hooks/use-category/use-category.ts b/src/api-hooks/use-category/use-category.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0bf3729a7ba923cf51b3d87d90346409d1c9875 --- /dev/null +++ b/src/api-hooks/use-category/use-category.ts @@ -0,0 +1,34 @@ +import { getCategories } from '@/utils'; +import { MenuList, Stylebook } from '@/interfaces'; + +import { useCatalog } from '../use-catalog'; + +interface UseCategoryResult { + newStylebook: Stylebook | null; + category: MenuList | null; + isError: boolean; + error: Error | null; + isLoading: boolean; + isSuccess: boolean; +} + +export const useCategory = ( + shop: string, + tabIndex: number, + categoryIndex: number, +): UseCategoryResult => { + const { data, isError, error, isLoading, isSuccess } = useCatalog(shop); + const categories = isSuccess && data ? getCategories(data) : null; + const category = + isSuccess && categories ? categories[tabIndex]?.lists[categoryIndex] : null; + const newStylebook = isSuccess && data ? data.shop?.stylebook : null; + + return { + newStylebook, + category, + isError, + error, + isLoading, + isSuccess, + }; +}; diff --git a/src/api-hooks/use-login/index.ts b/src/api-hooks/use-login/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8e469f149a29676abee89360e6115f8e2586b1f --- /dev/null +++ b/src/api-hooks/use-login/index.ts @@ -0,0 +1 @@ +export * from './use-login'; diff --git a/src/api-hooks/use-login/use-login.tsx b/src/api-hooks/use-login/use-login.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cf6fb149022874874f35820295f140af0e490cf9 --- /dev/null +++ b/src/api-hooks/use-login/use-login.tsx @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postLogin } from '@/api'; +import { ILogin, ILoginResponse } from '@/interfaces'; + +export const useLoginMutation = ( + handleError: (error: Error) => void, + handleSuccess: (data: ILoginResponse) => void, +) => + useMutation({ + mutationKey: ['login'], + mutationFn: (data: ILogin) => postLogin(data), + onError: (error) => handleError(error), + onSuccess: (data) => handleSuccess(data), + }); diff --git a/src/api-hooks/use-logout/index.ts b/src/api-hooks/use-logout/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1057fbf32ee2f7cee1888f69b1b4130fe03d5b7b --- /dev/null +++ b/src/api-hooks/use-logout/index.ts @@ -0,0 +1 @@ +export * from './use-logout'; diff --git a/src/api-hooks/use-logout/use-logout.tsx b/src/api-hooks/use-logout/use-logout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6bf5c0f95b2736997c21de85dac421c347d68037 --- /dev/null +++ b/src/api-hooks/use-logout/use-logout.tsx @@ -0,0 +1,26 @@ +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/navigation'; + +import { logout } from '@/store'; +import { useAppDispatch } from '@/hooks'; +import { getLogout } from '@/api'; + +export const useLogout = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const handleLogout = async () => { + try { + await getLogout(); + dispatch(logout()); + } catch (error) { + if (isAxiosError(error) && error.code === '404') router.replace('/404'); + if (isAxiosError(error) && error.code === '500') { + router.replace('/server-error'); + dispatch(logout()); + } + throw new Error('Что-то пошло не так'); + } + }; + + return handleLogout; +}; diff --git a/src/api-hooks/use-payment/index.ts b/src/api-hooks/use-payment/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4051e7ae168f2fe63d88b3230035d17cdb79d6f2 --- /dev/null +++ b/src/api-hooks/use-payment/index.ts @@ -0,0 +1 @@ +export * from './use-payment'; diff --git a/src/api-hooks/use-payment/use-payment.ts b/src/api-hooks/use-payment/use-payment.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d362d8300d134acb530c6ba30ad18f059d1b44d --- /dev/null +++ b/src/api-hooks/use-payment/use-payment.ts @@ -0,0 +1,25 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postPayment } from '@/api'; + +interface BasketItem { + slug: string | null; + count: number | null; +} + +interface IPaymentProps { + shop: string | null; + time: number | null; + basket: BasketItem[] | []; +} + +export const usePaymentMutation = ( + handleError: (error: Error) => void, + handleSuccess: () => void, +) => + useMutation({ + mutationKey: ['users'], + mutationFn: (reqData: IPaymentProps) => postPayment(reqData), + onError: (error) => handleError(error), + onSuccess: handleSuccess, + }); diff --git a/src/api-hooks/use-product/index.ts b/src/api-hooks/use-product/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2415b66afb92c98ce37a8129aca658ec31c4b8bb --- /dev/null +++ b/src/api-hooks/use-product/index.ts @@ -0,0 +1 @@ +export * from './use-product'; diff --git a/src/api-hooks/use-product/use-product.ts b/src/api-hooks/use-product/use-product.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1b6f7a35d141e6cf47933f52000d80a92a71c05 --- /dev/null +++ b/src/api-hooks/use-product/use-product.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getProduct } from '@/api'; +import { OperatingHours, Stylebook, IProduct } from '@/interfaces'; + +interface UseProductResult { + stylebook: Stylebook | null; + product: IProduct | null; + isError: boolean; + error: Error | null; + isLoading: boolean; + isSuccess: boolean; + mode: OperatingHours | null; +} + +export const useProduct = (shop: string, item: string): UseProductResult => { + const { data, isError, error, isLoading, isSuccess } = useQuery({ + queryKey: ['catalog', shop, item], + queryFn: async () => getProduct(shop, item), + }); + const stylebook = isSuccess && data ? data.shop?.stylebook : null; + const product = isSuccess && data ? data.product : null; + const mode = isSuccess && data ? data.shop.mode : null; + + return { + mode, + stylebook, + product, + isError, + error, + isLoading, + isSuccess, + }; +}; diff --git a/src/api-hooks/use-recommendations/index.ts b/src/api-hooks/use-recommendations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d51b89120f26ba276459d53c5536e83ed4c3b70 --- /dev/null +++ b/src/api-hooks/use-recommendations/index.ts @@ -0,0 +1 @@ +export * from './use-recommendations'; diff --git a/src/api-hooks/use-recommendations/use-recommendations.ts b/src/api-hooks/use-recommendations/use-recommendations.ts new file mode 100644 index 0000000000000000000000000000000000000000..478da1be1515de355e49607d4652ca77e0beb2bd --- /dev/null +++ b/src/api-hooks/use-recommendations/use-recommendations.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; + +import { postRecommendations } from '@/api'; + +interface IRecommendationsProps { + shop: string; + currentItem: string; +} + +export const useRecommendations = (data: IRecommendationsProps) => + useQuery({ + queryKey: ['promo', data.shop, data.currentItem], + queryFn: () => postRecommendations(data), + }); diff --git a/src/api-hooks/use-register/index.ts b/src/api-hooks/use-register/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..335fa0a3bbd95c398b59d209f9978302e46b2ad6 --- /dev/null +++ b/src/api-hooks/use-register/index.ts @@ -0,0 +1 @@ +export * from './use-register'; diff --git a/src/api-hooks/use-register/use-register.ts b/src/api-hooks/use-register/use-register.ts new file mode 100644 index 0000000000000000000000000000000000000000..dced1c219762f532bb6bf81fb4435d7e155d7210 --- /dev/null +++ b/src/api-hooks/use-register/use-register.ts @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postRegister } from '@/api'; +import { IGetUserResponse, TUser } from '@/interfaces'; + +export const useRegisterMutation = ( + handleError: (error: Error) => void, + handleSuccess: (data: IGetUserResponse) => void, +) => + useMutation({ + mutationKey: ['signup'], + mutationFn: (data: TUser) => postRegister(data), + onError: (error) => handleError(error), + onSuccess: (data) => handleSuccess(data), + }); diff --git a/src/api-hooks/use-shops/index.ts b/src/api-hooks/use-shops/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..40bd04848e4f9c074704d780893cdd24c6408416 --- /dev/null +++ b/src/api-hooks/use-shops/index.ts @@ -0,0 +1 @@ +export * from './use-shops'; diff --git a/src/api-hooks/use-shops/use-shops.ts b/src/api-hooks/use-shops/use-shops.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d3fbd92a06fa4c49e8a917b4e3e85ed37cf5ad8 --- /dev/null +++ b/src/api-hooks/use-shops/use-shops.ts @@ -0,0 +1,11 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; + +import { getShops } from '@/api'; + +export const useShops = () => + useQuery({ + queryKey: ['shops'], + queryFn: getShops, + }); diff --git a/src/api-hooks/use-user/index.ts b/src/api-hooks/use-user/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..724dd72bb90a33ee229889e648601a907c0bcf9b --- /dev/null +++ b/src/api-hooks/use-user/index.ts @@ -0,0 +1 @@ +export * from './use-user'; diff --git a/src/api-hooks/use-user/use-user.ts b/src/api-hooks/use-user/use-user.ts new file mode 100644 index 0000000000000000000000000000000000000000..88470428ca36f68985b5d155ea39652a3207fee8 --- /dev/null +++ b/src/api-hooks/use-user/use-user.ts @@ -0,0 +1,18 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { getUser, putUser } from '@/api'; + +import { TProfileSchema } from '../../components/profile/schema'; + +export const useUser = (userId: string) => + useQuery({ + queryKey: ['users', userId], + queryFn: () => getUser(userId), + enabled: false, + }); + +export const useUserMutation = (userId: string) => + useMutation({ + mutationKey: ['users', userId], + mutationFn: (reqData: TProfileSchema) => putUser(userId, reqData), + }); diff --git a/src/api/fetchers/fetch-auth/fetch-auth.ts b/src/api/fetchers/fetch-auth/fetch-auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b84ec2c8a594195a549aecf053f3d4f67c84a20 --- /dev/null +++ b/src/api/fetchers/fetch-auth/fetch-auth.ts @@ -0,0 +1,102 @@ +import { AxiosError, isAxiosError } from 'axios'; + +import { IGetUserResponse, ILogin, ILoginResponse, TUser } from '@/interfaces'; + +import { instance } from '../../instance'; +import { TProfileSchema } from '../../../components/profile/schema'; + +interface ILogoutResponse { + success: boolean; + message: string; +} + +export const postRegister = async (reqData: TUser) => { + try { + const { data } = await instance.postForm( + '/sign-up', + reqData, + ); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.statusText, + error.response?.status.toString(), + ); + } + throw new Error('Что-то пошло не так'); + } +}; + +export const postLogin = async (reqData: ILogin) => { + try { + const { data } = await instance.postForm('/login', reqData); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.statusText, + error.response?.status.toString(), + ); + } + + throw new Error('Что-то пошло не так'); + } +}; + +export const getLogout = async () => { + try { + const { data } = await instance.get('/logout'); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.statusText, + error.response?.status.toString(), + ); + } + + throw new Error('Что-то пошло не так'); + } +}; + +export const putUser = async (userID: string, reqData: TProfileSchema) => { + try { + const formBody = new URLSearchParams(reqData).toString(); + const { data } = await instance.put( + `/users/${userID}`, + formBody, + ); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.statusText, + error.response?.status.toString(), + ); + } + + throw new Error('Что-то пошло не так'); + } +}; + +export const getUser = async (userID: string) => { + try { + const { data } = await instance.get(`/users/${userID}`); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.statusText, + error.response?.status.toString(), + ); + } + + throw new Error('Что-то пошло не так'); + } +}; diff --git a/src/api/fetchers/fetch-auth/index.ts b/src/api/fetchers/fetch-auth/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..824b6bd20921201cfb280bcfc9dbb9798e6c5d27 --- /dev/null +++ b/src/api/fetchers/fetch-auth/index.ts @@ -0,0 +1 @@ +export * from './fetch-auth'; diff --git a/src/api/fetchers/fetch-catalog/fetch-catalog.ts b/src/api/fetchers/fetch-catalog/fetch-catalog.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ccb290de70d0e097e6d2418f248ff3fd22b80c3 --- /dev/null +++ b/src/api/fetchers/fetch-catalog/fetch-catalog.ts @@ -0,0 +1,70 @@ +import { AxiosError, isAxiosError } from 'axios'; + +import { Catalog, IProductData, IProductRecommendations } from '@/interfaces'; + +import { instance } from '../../instance'; + +interface IRecommendationsProps { + shop: string; + currentItem: string; +} + +export const getCatalog = async (shop: string) => { + try { + const { data } = await instance.get(`/catalog/${shop}`); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.data.message, + error.code?.toString(), + ); + } + + throw new Error('Не работает!'); + } +}; + +export const getProduct = async (shop: string, item: string) => { + try { + const { data } = await instance.get( + `/catalog/${shop}/${item}`, + ); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.data.message, + error.code?.toString(), + ); + } + throw new Error('Не работает!'); + } +}; + +export const postRecommendations = async (reqData: IRecommendationsProps) => { + try { + const { data } = await instance.post( + '/catalog/promo', + reqData, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.data.message, + error.code?.toString(), + ); + } + + throw new Error('Не работает!'); + } +}; diff --git a/src/api/fetchers/fetch-catalog/index.ts b/src/api/fetchers/fetch-catalog/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..75f6466592d7aefd32ca6297258b2c5baa42b3e4 --- /dev/null +++ b/src/api/fetchers/fetch-catalog/index.ts @@ -0,0 +1 @@ +export * from './fetch-catalog'; diff --git a/src/api/fetchers/fetch-payment/fetch-payment.ts b/src/api/fetchers/fetch-payment/fetch-payment.ts new file mode 100644 index 0000000000000000000000000000000000000000..118aaedb21fd35595f17849e0f3674682b433323 --- /dev/null +++ b/src/api/fetchers/fetch-payment/fetch-payment.ts @@ -0,0 +1,31 @@ +import { AxiosError, isAxiosError } from 'axios'; + +import { instance } from '../../instance'; + +interface BasketItem { + slug: string | null; + count: number | null; +} + +interface IPaymentProps { + shop: string | null; + time: number | null; + basket: BasketItem[] | []; +} + +export const postPayment = async (reqData: IPaymentProps) => { + try { + const { data } = await instance.post('/payment', reqData); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.data.message, + error.code?.toString(), + ); + } + + throw new Error('Не работает!'); + } +}; diff --git a/src/api/fetchers/fetch-payment/index.ts b/src/api/fetchers/fetch-payment/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..63003a548f9f5c5fa0da9164cf333f8076b97ae1 --- /dev/null +++ b/src/api/fetchers/fetch-payment/index.ts @@ -0,0 +1 @@ +export * from './fetch-payment'; diff --git a/src/api/fetchers/fetch-shops/fetch-shops.ts b/src/api/fetchers/fetch-shops/fetch-shops.ts new file mode 100644 index 0000000000000000000000000000000000000000..baf3b835122a9b558340c5f2c0a560ee4cc53d24 --- /dev/null +++ b/src/api/fetchers/fetch-shops/fetch-shops.ts @@ -0,0 +1,22 @@ +import { AxiosError, isAxiosError } from 'axios'; + +import { Shop } from '@/interfaces'; + +import { instance } from '../../instance'; + +export const getShops = async (): Promise => { + try { + const { data } = await instance.get('/shops'); + + return data; + } catch (error) { + if (isAxiosError(error)) { + throw new AxiosError( + error.response?.data.message, + error.code?.toString(), + ); + } + + throw new Error('Не работает!'); + } +}; diff --git a/src/api/fetchers/fetch-shops/index.ts b/src/api/fetchers/fetch-shops/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..14a203e642b540ec8aa0808f7f511afe3bca2ff3 --- /dev/null +++ b/src/api/fetchers/fetch-shops/index.ts @@ -0,0 +1 @@ +export * from './fetch-shops'; diff --git a/src/api/fetchers/index.ts b/src/api/fetchers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b8f30b9490aa1bcaf2910e7bf44fcccc0a417c1 --- /dev/null +++ b/src/api/fetchers/index.ts @@ -0,0 +1,4 @@ +export * from './fetch-catalog'; +export * from './fetch-shops'; +export * from './fetch-payment'; +export * from './fetch-auth'; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e0498b630b1091b9e9c73036c1c52546e807f81 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1 @@ +export * from './fetchers'; diff --git a/src/api/instance.ts b/src/api/instance.ts new file mode 100644 index 0000000000000000000000000000000000000000..253c71cdbc738689cdd29d9bdb32a1363ea330e7 --- /dev/null +++ b/src/api/instance.ts @@ -0,0 +1,70 @@ +import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; + +import { setToken, store } from '@/store'; +import { PROTECTED_ROUTES } from '@/utils'; + +interface IRefreshResponse { + success: boolean; + accessToken: string; +} + +const { API_URL = 'http://localhost:3001/api/' } = process.env; + +const instance = axios.create({ + baseURL: API_URL, +}); + +instance.interceptors.request.use( + (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { + if (!PROTECTED_ROUTES.some((route) => config.url?.includes(route))) + return config; + + const state = store.getState(); + const token = state.user.accessToken; + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + Promise.reject(error); + }, +); + +let isReturn = false; + +instance.interceptors.response.use( + (response: AxiosResponse): AxiosResponse => response, + async (error) => { + const originalRequest = error.config; + + const oldTokenRequestHedears = originalRequest.headers.Authorization; + const oldTokenStore = store.getState().user.accessToken; + + if (error.response.status === 401 && !isReturn) { + isReturn = true; + + try { + const { data } = await instance.get('/refresh', { + headers: { + Authorization: oldTokenRequestHedears || oldTokenStore, + }, + }); + originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; + store.dispatch(setToken({ data: data.accessToken })); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(instance(originalRequest)); + }, 2000); + }); + } catch (refreshError) { + return Promise.reject(refreshError); + } + } + return Promise.reject(error); + }, +); + +export { instance }; diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c57aecf8c2d9b231f11877e3751cfbf8cfc30a3c --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { ErrorPage } from '@/components'; + +const Error = () => { + return ; +}; + +export default Error; diff --git a/src/app/globals.css b/src/app/globals.css index f4bd77c0ccacd185f097e1fe5a2975992fb989a1..82ae2255ee9f9906ba45805a666b0dc8ccea33a9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,107 +1,85 @@ -:root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", - "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; +@import url('../vendor/normalize/normalize.css'); +@import url('../vendor/fonts/fonts.css'); - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); +:root { + --family: Gilroy, sans-serif; + --family-secondary: Comfortaa, sans-serif; - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); + --white: #f3f4f0; + --gray-plus: #bebebe; + --gray: #959595; + --black-plus: #383838; + --black-plus-rgb: 56, 56, 56; + --black: #242424; + --brown: #cb9666; + --pink-plus: #f3b6d1; + --pink: #f669a2; + --red-plus: #fe6a69; + --red: #fa5452; + --yellow-plus: #ffc633; + --yellow: #ffb92a; + --aquamarine-plus: #a5dfdd; + --aquamarine: #23bcc7; + --pistachio: #b1d465; + --green-plus: #5dd1b7; + --green: #51c7a5; + --green-minus: #14ad99; + --violet-plus: #a36ebe; + --violet: #9e50c7; + --blue-plus: #01a9d5; + --blue: #027ec2; + --sky-blue: #a7c3f8; +} - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; +* { + margin: 0; + padding: 0; } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; +h1, +h2, +h3 { + margin: 0; +} - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); +ol, +ul { + list-style: none; +} - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); +img { + max-width: 100%; + display: block; +} - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } +a { + text-decoration: none; + color: inherit; } -* { - box-sizing: border-box; - padding: 0; - margin: 0; +table { + border-collapse: collapse; + border-spacing: 0; } -html, -body { - max-width: 100vw; - overflow-x: hidden; +button { + cursor: pointer; } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); +button, +input { + border: none; + background: none; + box-shadow: none; } -a { - color: inherit; - text-decoration: none; +input[type='text'], +input[type='search'] { + -webkit-appearance: none; + appearance: none; } -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } +fieldset { + border: none; + padding: 0; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4917207766d306e7bba1642cdd85652c0f89675e..c96ac0bdca0bec6cc2795ced824c05cce8e38c7b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,29 +1,55 @@ -import React from 'react'; -import { Inter } from 'next/font/google'; +import { ReactNode } from 'react'; +import 'swiper/swiper-bundle.css'; +import { + ContentMain, + Footer, + Header, + MainContainer, + SideBar, + StylebookProvider, + Widget, +} from '@/components'; + +import StoreProvider from './store-provider'; import './globals.css'; -import StyledComponentsRegistry from './lib/registry'; +import StyledComponentsRegistry from '../lib/registry'; +import ReactQueryProvider from './query-provider'; import type { Metadata } from 'next'; - -const inter = Inter({ subsets: ['latin'] }); - const RootLayout = ({ children, }: Readonly<{ - children: React.ReactNode; + children: ReactNode; }>) => ( - - - { children } + + + + + + + + + + +
+ {children} +