├── apps ├── admin │ ├── public │ │ ├── robots.txt │ │ └── fonts │ │ │ ├── 400.woff2 │ │ │ └── 700.woff2 │ ├── .gitignore │ ├── prettier.config.js │ ├── stylelint.config.js │ ├── src │ │ ├── modules │ │ │ ├── customer │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── CustomerPage.spec.ts.snap │ │ │ │ │ │ └── CustomerListPage.spec.ts.snap │ │ │ │ │ └── CustomerPage.vue │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ │ └── components │ │ │ │ │ └── __snapshots__ │ │ │ │ │ └── CustomerList.spec.ts.snap │ │ │ ├── auth │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── LoginPage.spec.ts.snap │ │ │ │ │ │ └── SetupPage.spec.ts.snap │ │ │ │ │ ├── LoginPage.vue │ │ │ │ │ ├── SetupPage.vue │ │ │ │ │ ├── LoginPage.spec.ts │ │ │ │ │ └── SetupPage.spec.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ ├── LoginForm.spec.ts.snap │ │ │ │ │ │ └── SetupForm.spec.ts.snap │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ ├── banner │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── BannerCreatePage.spec.ts.snap │ │ │ │ │ │ ├── BannerEditPage.spec.ts.snap │ │ │ │ │ │ └── BannerListPage.spec.ts.snap │ │ │ │ │ ├── BannerCreatePage.spec.ts │ │ │ │ │ ├── BannerCreatePage.vue │ │ │ │ │ └── BannerEditPage.vue │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ ├── manager │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── ManagerCreatePage.spec.ts.snap │ │ │ │ │ │ ├── ManagerEditPage.spec.ts.snap │ │ │ │ │ │ └── ManagerListPage.spec.ts.snap │ │ │ │ │ ├── ManagerCreatePage.spec.ts │ │ │ │ │ └── ManagerCreatePage.vue │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ ├── ManagerForm.spec.ts.snap │ │ │ │ │ │ └── ManagerList.spec.ts.snap │ │ │ │ ├── fixtures │ │ │ │ │ └── index.ts │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ ├── product │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── ProductCreatePage.spec.ts.snap │ │ │ │ │ │ ├── ProductEditPage.spec.ts.snap │ │ │ │ │ │ └── ProductListPage.spec.ts.snap │ │ │ │ │ ├── ProductCreatePage.spec.ts │ │ │ │ │ ├── ProductCreatePage.vue │ │ │ │ │ └── ProductEditPage.vue │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ └── ProductFieldsForm.spec.ts.snap │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ ├── common │ │ │ │ ├── components │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── App.spec.ts.snap │ │ │ │ │ │ ├── ImagePreview.spec.ts.snap │ │ │ │ │ │ ├── FormButtons.spec.ts.snap │ │ │ │ │ │ └── EntitiesCount.spec.ts.snap │ │ │ │ │ └── EntitiesCount.vue │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── styles │ │ │ │ │ └── main.scss │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── ErrorPage.spec.ts.snap │ │ │ │ │ │ └── MainPage.spec.ts.snap │ │ │ │ │ ├── ErrorPage.vue │ │ │ │ │ ├── MainPage.vue │ │ │ │ │ ├── MainPage.spec.ts │ │ │ │ │ └── ErrorPage.spec.ts │ │ │ │ ├── router │ │ │ │ │ ├── index.ts │ │ │ │ │ └── routes.ts │ │ │ │ ├── services │ │ │ │ │ └── index.ts │ │ │ │ └── fixtures │ │ │ │ │ └── index.ts │ │ │ ├── manufacturer │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── ManufacturerCreatePage.spec.ts.snap │ │ │ │ │ │ ├── ManufacturerEditPage.spec.ts.snap │ │ │ │ │ │ └── ManufacturerListPage.spec.ts.snap │ │ │ │ │ ├── ManufacturerCreatePage.spec.ts │ │ │ │ │ ├── ManufacturerCreatePage.vue │ │ │ │ │ └── ManufacturerEditPage.vue │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── fixtures │ │ │ │ │ └── index.ts │ │ │ │ └── services │ │ │ │ │ └── index.ts │ │ │ ├── order │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── OrderPage.spec.ts.snap │ │ │ │ │ │ └── OrderListPage.spec.ts.snap │ │ │ │ │ └── OrderPage.vue │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ │ └── components │ │ │ │ │ └── __snapshots__ │ │ │ │ │ └── OrderList.spec.ts.snap │ │ │ ├── category │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ ├── CategoryCreatePage.spec.ts │ │ │ │ │ ├── CategoryCreatePage.vue │ │ │ │ │ ├── CategoryEditPage.vue │ │ │ │ │ └── CategoryListPage.vue │ │ │ │ ├── components │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── CategoryFieldForm.spec.ts.snap │ │ │ │ │ └── CategoryList.vue │ │ │ │ └── services │ │ │ │ │ ├── api.ts │ │ │ │ │ └── index.ts │ │ │ └── layout │ │ │ │ ├── components │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── LayoutEmpty.spec.ts.snap │ │ │ │ │ ├── NavItem.spec.ts.snap │ │ │ │ │ ├── TheSearch.spec.ts.snap │ │ │ │ │ ├── PageTitle.spec.ts.snap │ │ │ │ │ ├── LayoutDefault.spec.ts.snap │ │ │ │ │ ├── TheHeader.spec.ts.snap │ │ │ │ │ └── NavList.spec.ts.snap │ │ │ │ ├── LayoutEmpty.vue │ │ │ │ ├── NavList.vue │ │ │ │ ├── LayoutEmpty.spec.ts │ │ │ │ ├── LayoutDefault.spec.ts │ │ │ │ ├── TheSearch.vue │ │ │ │ ├── PageTitle.vue │ │ │ │ ├── NavList.spec.ts │ │ │ │ ├── NavItem.vue │ │ │ │ └── LayoutDefault.vue │ │ │ │ ├── interface │ │ │ │ └── index.ts │ │ │ │ └── icons │ │ │ │ ├── nav-manufacturer.svg │ │ │ │ ├── nav-order.svg │ │ │ │ ├── nav-manager.svg │ │ │ │ ├── nav-product.svg │ │ │ │ ├── nav-main.svg │ │ │ │ ├── nav-banner.svg │ │ │ │ ├── nav-category.svg │ │ │ │ └── nav-customer.svg │ │ ├── env.d.ts │ │ └── main.ts │ ├── .env.example │ ├── tsconfig.node.json │ ├── index.html │ ├── vitest.setup.ts │ ├── tsconfig.json │ └── eslint.config.js ├── site │ ├── .gitignore │ ├── src │ │ ├── modules │ │ │ ├── cart │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── composables │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── CartItemList.vue │ │ │ │ │ ├── CartHeaderButton.vue │ │ │ │ │ └── CartItemCount.vue │ │ │ │ └── pages │ │ │ │ │ └── CartPage.vue │ │ │ ├── category │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── CategoryPopularList.vue │ │ │ │ │ ├── CategoryCard.vue │ │ │ │ │ └── CategoryCatalogList.vue │ │ │ │ ├── services │ │ │ │ │ ├── api.ts │ │ │ │ │ └── index.ts │ │ │ │ └── pages │ │ │ │ │ └── CategoryListPage.vue │ │ │ ├── manufacturer │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── ManufacturerPopularList.vue │ │ │ │ │ ├── ManufacturerCatalogList.vue │ │ │ │ │ └── ManufacturerCard.vue │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ ├── order │ │ │ │ ├── contants │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── OrderList.vue │ │ │ │ │ └── OrderElement.vue │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ ├── common │ │ │ │ ├── images │ │ │ │ │ └── map.jpg │ │ │ │ ├── styles │ │ │ │ │ └── main.scss │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ │ └── ErrorPage.vue │ │ │ │ ├── services │ │ │ │ │ ├── index.ts │ │ │ │ │ └── api.ts │ │ │ │ └── router │ │ │ │ │ ├── index.ts │ │ │ │ │ └── routes.ts │ │ │ ├── product │ │ │ │ ├── interface │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── ProductPopularList.vue │ │ │ │ │ └── ProductAddToCartButton.vue │ │ │ │ └── constants │ │ │ │ │ └── index.ts │ │ │ ├── banner │ │ │ │ └── services │ │ │ │ │ ├── api.ts │ │ │ │ │ └── index.ts │ │ │ ├── customer │ │ │ │ ├── pages │ │ │ │ │ ├── WatchedProductsPage.vue │ │ │ │ │ ├── ProfilePage.vue │ │ │ │ │ ├── FavouritesPage.vue │ │ │ │ │ ├── OrderPage.vue │ │ │ │ │ └── OrdersPage.vue │ │ │ │ ├── components │ │ │ │ │ ├── CustomerWatchedProducts.vue │ │ │ │ │ └── CustomerNav.vue │ │ │ │ └── constants │ │ │ │ │ └── index.ts │ │ │ ├── layout │ │ │ │ ├── components │ │ │ │ │ ├── LayoutEmpty.vue │ │ │ │ │ ├── PageTitle.vue │ │ │ │ │ ├── TheSearch.vue │ │ │ │ │ ├── LayoutDefault.vue │ │ │ │ │ └── FullscreenFilters.vue │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ └── icons │ │ │ │ │ ├── search.svg │ │ │ │ │ ├── login.svg │ │ │ │ │ ├── logout.svg │ │ │ │ │ ├── cart.svg │ │ │ │ │ ├── catalog.svg │ │ │ │ │ ├── filters.svg │ │ │ │ │ ├── profile.svg │ │ │ │ │ └── signup.svg │ │ │ ├── configuration │ │ │ │ ├── interface │ │ │ │ │ └── index.ts │ │ │ │ ├── routes │ │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── ConfigurationElement.vue │ │ │ │ │ ├── ConfigurationList.vue │ │ │ │ │ └── ConfigurationAuthor.vue │ │ │ │ ├── constants │ │ │ │ │ └── index.ts │ │ │ │ └── services │ │ │ │ │ └── index.ts │ │ │ └── auth │ │ │ │ ├── routes │ │ │ │ └── index.ts │ │ │ │ ├── pages │ │ │ │ ├── LoginPage.vue │ │ │ │ └── SignUpPage.vue │ │ │ │ ├── services │ │ │ │ ├── index.ts │ │ │ │ └── api.ts │ │ │ │ └── constants │ │ │ │ └── index.ts │ │ ├── env.d.ts │ │ └── main.ts │ ├── prettier.config.js │ ├── stylelint.config.js │ ├── public │ │ ├── pwa-180.png │ │ ├── pwa-192.png │ │ ├── pwa-512.png │ │ ├── fonts │ │ │ ├── 400.woff2 │ │ │ └── 700.woff2 │ │ └── robots.txt │ ├── .env.example │ ├── tsconfig.node.json │ ├── vitest.setup.ts │ ├── tsconfig.json │ └── eslint.config.js └── back │ ├── .gitignore │ ├── prettier.config.js │ ├── dump │ └── mhz │ │ ├── banners.bson │ │ ├── products.bson │ │ ├── categories.bson │ │ ├── manufacturers.bson │ │ ├── banners.metadata.json │ │ ├── products.metadata.json │ │ ├── categories.metadata.json │ │ └── manufacturers.metadata.json │ ├── .env.example │ ├── src │ ├── plugins │ │ ├── helmet.ts │ │ ├── rate.ts │ │ ├── multipart.ts │ │ ├── static.ts │ │ ├── cors.ts │ │ └── swagger.ts │ ├── helpers │ │ ├── deleteFile.ts │ │ ├── createThumb.ts │ │ ├── decodeToken.ts │ │ ├── resizeFile.ts │ │ ├── index.ts │ │ ├── addView.ts │ │ ├── deleteFile.spec.ts │ │ ├── addView.spec.ts │ │ ├── decodeToken.spec.ts │ │ ├── addProductToWatched.ts │ │ ├── createThumb.spec.ts │ │ └── resizeFile.spec.ts │ ├── index.ts │ ├── models │ │ ├── manager.ts │ │ ├── banner.ts │ │ ├── manufacturer.ts │ │ ├── order.ts │ │ ├── product.ts │ │ ├── category.ts │ │ └── customer.ts │ ├── routes │ │ ├── stats.ts │ │ └── search.ts │ ├── app.ts │ └── services │ │ └── manager.ts │ ├── nodemon.json │ ├── vitest.config.ts │ ├── tsconfig.json │ └── eslint.config.js ├── .gitignore ├── packages ├── bank │ ├── .gitignore │ ├── tsconfig.node.json │ ├── index.html │ ├── vite.config.ts │ ├── package.json │ └── tsconfig.json ├── countries │ ├── dist │ │ └── types │ │ │ └── index.d.ts │ └── package.json └── contracts │ ├── dist │ └── types │ │ └── index.d.ts │ └── package.json ├── .editorconfig ├── .changeset └── config.json ├── turbo.json ├── .gitattributes ├── README.md └── LICENSE /apps/admin/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .env 4 | .turbo 5 | .vscode -------------------------------------------------------------------------------- /apps/admin/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /coverage 4 | .env 5 | .turbo -------------------------------------------------------------------------------- /apps/site/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /coverage 4 | .env 5 | .turbo -------------------------------------------------------------------------------- /packages/bank/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | .idea 5 | .vscode/* -------------------------------------------------------------------------------- /apps/site/src/modules/cart/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CART = '/cart'; 2 | -------------------------------------------------------------------------------- /packages/countries/dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const countries: string[]; 2 | -------------------------------------------------------------------------------- /apps/admin/prettier.config.js: -------------------------------------------------------------------------------- 1 | export { prettier as default } from 'vue-linters-config'; 2 | -------------------------------------------------------------------------------- /apps/admin/stylelint.config.js: -------------------------------------------------------------------------------- 1 | export { stylelint as default } from 'vue-linters-config'; 2 | -------------------------------------------------------------------------------- /apps/back/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .turbo 3 | node_modules 4 | build 5 | public/upload 6 | coverage -------------------------------------------------------------------------------- /apps/back/prettier.config.js: -------------------------------------------------------------------------------- 1 | export { prettier as default } from 'vue-linters-config'; 2 | -------------------------------------------------------------------------------- /apps/site/prettier.config.js: -------------------------------------------------------------------------------- 1 | export { prettier as default } from 'vue-linters-config'; 2 | -------------------------------------------------------------------------------- /apps/site/stylelint.config.js: -------------------------------------------------------------------------------- 1 | export { stylelint as default } from 'vue-linters-config'; 2 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CUSTOMER = '/customers'; 2 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CATEGORY = '/categories'; 2 | -------------------------------------------------------------------------------- /packages/contracts/dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /apps/site/public/pwa-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/site/public/pwa-180.png -------------------------------------------------------------------------------- /apps/site/public/pwa-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/site/public/pwa-192.png -------------------------------------------------------------------------------- /apps/site/public/pwa-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/site/public/pwa-512.png -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_MANUFACTURER = '/manufacturers'; 2 | -------------------------------------------------------------------------------- /apps/back/dump/mhz/banners.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/back/dump/mhz/banners.bson -------------------------------------------------------------------------------- /apps/back/dump/mhz/products.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/back/dump/mhz/products.bson -------------------------------------------------------------------------------- /apps/site/public/fonts/400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/site/public/fonts/400.woff2 -------------------------------------------------------------------------------- /apps/site/public/fonts/700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/site/public/fonts/700.woff2 -------------------------------------------------------------------------------- /apps/admin/public/fonts/400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/admin/public/fonts/400.woff2 -------------------------------------------------------------------------------- /apps/admin/public/fonts/700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/admin/public/fonts/700.woff2 -------------------------------------------------------------------------------- /apps/back/dump/mhz/categories.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/back/dump/mhz/categories.bson -------------------------------------------------------------------------------- /apps/back/dump/mhz/manufacturers.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/back/dump/mhz/manufacturers.bson -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=false 5 | indent_style=space 6 | tab_width=2 7 | -------------------------------------------------------------------------------- /apps/site/src/modules/order/contants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CHECKOUT = '/checkout'; 2 | export const URL_PAYMENT = '/payment'; 3 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/images/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergunovs/mhz/HEAD/apps/site/src/modules/common/images/map.jpg -------------------------------------------------------------------------------- /apps/site/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /*?page 3 | Disallow: /cart 4 | Disallow: /customer 5 | Disallow: /checkout 6 | Disallow: /payment -------------------------------------------------------------------------------- /apps/admin/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API=http://localhost:5000/api 2 | VITE_PATH_UPLOAD=http://localhost:5000/upload 3 | VITE_VERSION=${npm_package_version} 4 | VITE_CURRENCY=₽ -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_LOGIN = '/'; 2 | export const URL_SETUP = '/setup'; 3 | 4 | export const TOKEN_NAME = 'mhz_token'; 5 | -------------------------------------------------------------------------------- /apps/site/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API=http://localhost:5000/api 2 | VITE_PATH_UPLOAD=http://localhost:5000/upload 3 | VITE_VERSION=${npm_package_version} 4 | VITE_CURRENCY=₽ -------------------------------------------------------------------------------- /apps/site/src/modules/product/interface/index.ts: -------------------------------------------------------------------------------- 1 | export interface IProductSortOption { 2 | _id: string; 3 | title: string; 4 | value: string; 5 | isAsc: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /apps/back/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE=mhz 2 | SECRET=bgnb90gu0etKNLIHbon08f9d76b89dfbyfbhrrejnregkjenb 3 | PORT=5000 4 | SITE_URL=http://localhost:8081 5 | ADMIN_URL=http://localhost:8080 6 | IS_DEV=true -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_BANNER = '/banners'; 2 | export const URL_BANNER_CREATE = `${URL_BANNER}/create`; 3 | export const URL_BANNER_EDIT = `${URL_BANNER}/edit`; 4 | -------------------------------------------------------------------------------- /apps/back/src/plugins/helmet.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import helmet from '@fastify/helmet'; 3 | 4 | export default fp(async function (fastify) { 5 | fastify.register(helmet); 6 | }); 7 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_MANAGER = '/managers'; 2 | export const URL_MANAGER_CREATE = `${URL_MANAGER}/create`; 3 | export const URL_MANAGER_EDIT = `${URL_MANAGER}/edit`; 4 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_PRODUCT = '/products'; 2 | export const URL_PRODUCT_CREATE = `${URL_PRODUCT}/create`; 3 | export const URL_PRODUCT_EDIT = `${URL_PRODUCT}/edit`; 4 | -------------------------------------------------------------------------------- /apps/back/dump/mhz/banners.metadata.json: -------------------------------------------------------------------------------- 1 | {"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"d4e9623fee2c416eabb6b6e0295847a8","collectionName":"banners","type":"collection"} -------------------------------------------------------------------------------- /apps/back/dump/mhz/products.metadata.json: -------------------------------------------------------------------------------- 1 | {"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"6a59ce48b05744d98d923386a6c5da7d","collectionName":"products","type":"collection"} -------------------------------------------------------------------------------- /apps/back/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "exec": "esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --format=esm && node build", 5 | "legacyWatch": true 6 | } 7 | -------------------------------------------------------------------------------- /apps/back/dump/mhz/categories.metadata.json: -------------------------------------------------------------------------------- 1 | {"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"f586e0dc06ed4d1ca8a19b3e836d6bf5","collectionName":"categories","type":"collection"} -------------------------------------------------------------------------------- /apps/back/dump/mhz/manufacturers.metadata.json: -------------------------------------------------------------------------------- 1 | {"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"1664ce20b153486381c149b9a2823f77","collectionName":"manufacturers","type":"collection"} -------------------------------------------------------------------------------- /apps/back/src/helpers/deleteFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | export function deleteFile(filename?: string) { 5 | fs.unlinkSync(path.resolve(`./public/upload/${filename}`)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/components/__snapshots__/App.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`App > matches snapshot 1`] = ` 4 | "
5 | 6 |
" 7 | `; 8 | -------------------------------------------------------------------------------- /apps/back/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | cache: false, 6 | clearMocks: true, 7 | include: ['**/*.spec.ts'], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /apps/back/src/plugins/rate.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import rateLimit from '@fastify/rate-limit'; 3 | 4 | export default fp(async function (fastify) { 5 | fastify.register(rateLimit, { max: 100, timeWindow: 20000 }); 6 | }); 7 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_MAIN = '/main'; 2 | export const URL_ERROR = '/404'; 3 | 4 | export const PATH_UPLOAD = import.meta.env.VITE_PATH_UPLOAD; 5 | export const CURRENCY = import.meta.env.VITE_CURRENCY; 6 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_MANUFACTURER = '/manufacturers'; 2 | export const URL_MANUFACTURER_CREATE = `${URL_MANUFACTURER}/create`; 3 | export const URL_MANUFACTURER_EDIT = `${URL_MANUFACTURER}/edit`; 4 | -------------------------------------------------------------------------------- /apps/back/src/plugins/multipart.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import multipart from '@fastify/multipart'; 3 | 4 | export default fp(async function (fastify) { 5 | fastify.register(multipart, { limits: { fileSize: 10000000 } }); 6 | }); 7 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/pages/__snapshots__/LoginPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`LoginPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
" 7 | `; 8 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/pages/__snapshots__/SetupPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`SetupPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
" 7 | `; 8 | -------------------------------------------------------------------------------- /packages/bank/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ES2022", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/admin/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | const component: DefineComponent; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { TOrderStatus } from 'mhz-contracts'; 2 | 3 | export const URL_ORDER = '/orders'; 4 | 5 | export const ORDER_CANCELLED: TOrderStatus = 'cancelled'; 6 | export const ORDER_COMPLETED: TOrderStatus = 'completed'; 7 | -------------------------------------------------------------------------------- /apps/site/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | const component: DefineComponent; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /apps/site/src/modules/cart/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CART } from '@/cart/constants'; 2 | 3 | export const cartRoutes = [ 4 | { 5 | path: URL_CART, 6 | name: 'Cart', 7 | component: () => import('@/cart/pages/CartPage.vue'), 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/styles/main.scss: -------------------------------------------------------------------------------- 1 | @use 'mhz-ui/dist/colors'; 2 | @use 'mhz-ui/dist/fonts'; 3 | @use 'mhz-ui/dist/breakpoints'; 4 | @use 'mhz-ui/dist/transitions'; 5 | @use 'mhz-ui/dist/base'; 6 | 7 | * { 8 | font-family: 'base', sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/styles/main.scss: -------------------------------------------------------------------------------- 1 | @use 'mhz-ui/dist/colors'; 2 | @use 'mhz-ui/dist/fonts'; 3 | @use 'mhz-ui/dist/breakpoints'; 4 | @use 'mhz-ui/dist/transitions'; 5 | @use 'mhz-ui/dist/base'; 6 | 7 | * { 8 | font-family: 'base', sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/pages/__snapshots__/ErrorPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ErrorPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
Error
7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_MAIN = '/'; 2 | export const URL_PRIVACY = '/privacy'; 3 | export const URL_ERROR = '/404'; 4 | 5 | export const PATH_UPLOAD = import.meta.env.VITE_PATH_UPLOAD; 6 | export const CURRENCY = import.meta.env.VITE_CURRENCY; 7 | -------------------------------------------------------------------------------- /apps/site/src/modules/banner/services/api.ts: -------------------------------------------------------------------------------- 1 | import { api } from 'mhz-helpers'; 2 | import { API_BANNER_ACTIVE, IBanner } from 'mhz-contracts'; 3 | 4 | export async function getBannersActiveApi() { 5 | const { data } = await api.get(API_BANNER_ACTIVE); 6 | 7 | return data; 8 | } 9 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/pages/WatchedProductsPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CATEGORY = '/categories'; 2 | export const URL_CATEGORY_CREATE = `${URL_CATEGORY}/create`; 3 | export const URL_CATEGORY_EDIT = `${URL_CATEGORY}/edit`; 4 | 5 | export const CATEGORY_FIELD_TYPE_OPTIONS = ['string', 'number', 'boolean']; 6 | -------------------------------------------------------------------------------- /apps/site/src/modules/product/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_PRODUCT } from '@/product/constants'; 2 | 3 | export const productRoutes = [ 4 | { 5 | path: `${URL_PRODUCT}/:product`, 6 | name: 'Product', 7 | component: () => import('@/product/pages/ProductPage.vue'), 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/LayoutEmpty.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`LayoutEmpty > matches snapshot 1`] = ` 4 | "
5 | 6 |
" 7 | `; 8 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/pages/__snapshots__/MainPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`MainPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/interface/index.ts: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from 'vue'; 2 | 3 | export interface INavItem { 4 | _id: string; 5 | url: string; 6 | title: string; 7 | icon: FunctionalComponent; 8 | } 9 | 10 | export interface IPageTitleLink { 11 | url: string; 12 | title?: string; 13 | } 14 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/components/LayoutEmpty.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/pages/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/pages/SetupPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /apps/back/src/helpers/createThumb.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | 3 | export async function createThumb(filename: string) { 4 | await sharp(`./public/upload/${filename}`) 5 | .resize(480) 6 | .webp({ quality: 64 }) 7 | .toFile(`./public/upload/thumb-${filename}.webp`); 8 | 9 | return `thumb-${filename}.webp`; 10 | } 11 | -------------------------------------------------------------------------------- /apps/site/src/modules/banner/services/index.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'mhz-helpers'; 2 | import { API_BANNER_ACTIVE } from 'mhz-contracts'; 3 | 4 | import { getBannersActiveApi } from '@/banner/services/api'; 5 | 6 | export function getBannersActive() { 7 | return useQuery({ queryKey: [API_BANNER_ACTIVE], queryFn: getBannersActiveApi }); 8 | } 9 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/interface/index.ts: -------------------------------------------------------------------------------- 1 | export interface IConfigurationError { 2 | category: string; 3 | field: string; 4 | } 5 | 6 | export interface IConfigurationCheck { 7 | error: IConfigurationError[]; 8 | message: string; 9 | } 10 | 11 | export type TMotherboardFormat = 'Micro-ATX' | 'Mini-ITX' | 'Standard-ATX'; 12 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CONFIGURATION } from '@/configuration/constants'; 2 | 3 | export const configurationRoutes = [ 4 | { 5 | path: `${URL_CONFIGURATION}/:configuration`, 6 | name: 'Configuration', 7 | component: () => import('@/configuration/pages/ConfigurationPage.vue'), 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/back/src/helpers/decodeToken.ts: -------------------------------------------------------------------------------- 1 | import type { IUserToken } from 'mhz-contracts'; 2 | 3 | export function decodeToken(decode?: (token: string) => IUserToken | null, authorizationHeader?: string) { 4 | const token = authorizationHeader ? authorizationHeader.split('Bearer ')[1] : undefined; 5 | 6 | return token && decode ? decode(token) : null; 7 | } 8 | -------------------------------------------------------------------------------- /packages/bank/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mhz Bank 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/pages/__snapshots__/BannerCreatePage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`BannerCreatePage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["mhz-admin", "mhz-back", "mhz-site"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/pages/__snapshots__/ManagerCreatePage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManagerCreatePage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/pages/__snapshots__/ProductCreatePage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ProductCreatePage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_LOGIN, URL_SETUP } from '@/auth/constants'; 2 | 3 | export const authRoutes = [ 4 | { path: URL_LOGIN, name: 'Login', component: () => import('@/auth/pages/LoginPage.vue'), meta: { layout: 'empty' } }, 5 | { path: URL_SETUP, name: 'Setup', component: () => import('@/auth/pages/SetupPage.vue'), meta: { layout: 'empty' } }, 6 | ]; 7 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/pages/ErrorPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/NavItem.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`NavItem > matches snapshot 1`] = ` 4 | " 5 | title 6 | " 7 | `; 8 | -------------------------------------------------------------------------------- /apps/back/src/helpers/resizeFile.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | 3 | import { deleteFile } from './deleteFile.js'; 4 | 5 | export async function resizeFile(filename: string, width: string) { 6 | await sharp(`./public/upload/${filename}`).resize(Number(width)).toFile(`./public/upload/resized-${filename}`); 7 | 8 | deleteFile(filename); 9 | 10 | return `resized-${filename}`; 11 | } 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/pages/__snapshots__/ManufacturerCreatePage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManufacturerCreatePage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue'; 2 | 3 | import { useQuery } from 'mhz-helpers'; 4 | import { API_SEARCH } from 'mhz-contracts'; 5 | 6 | import { searchApi } from '@/common/services/api'; 7 | 8 | export function search(query: Ref) { 9 | return useQuery({ queryKey: [API_SEARCH, query], queryFn: () => searchApi(query), enabled: false }); 10 | } 11 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/pages/__snapshots__/OrderPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`OrderPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/back/src/plugins/static.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import fp from 'fastify-plugin'; 4 | import staticF from '@fastify/static'; 5 | 6 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | export default fp(async function (fastify) { 9 | fastify.register(staticF, { root: path.join(dirname, '../../public') }); 10 | }); 11 | -------------------------------------------------------------------------------- /apps/back/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { addProductToWatched } from './addProductToWatched.js'; 2 | export { addView } from './addView.js'; 3 | export { createThumb } from './createThumb.js'; 4 | export { decodeToken } from './decodeToken.js'; 5 | export { deleteFile } from './deleteFile.js'; 6 | export { resizeFile } from './resizeFile.js'; 7 | export { paginate, getProductFilters } from './filtersAndPagination.js'; 8 | -------------------------------------------------------------------------------- /apps/back/src/plugins/cors.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import cors from '@fastify/cors'; 3 | 4 | export default fp(async function (fastify) { 5 | fastify.register(cors, { 6 | origin: [`${process.env.SITE_URL}`, `${process.env.ADMIN_URL}`], 7 | methods: 'GET,PATCH,POST,DELETE,OPTIONS', 8 | credentials: true, 9 | allowedHeaders: 'Content-Type,Authorization', 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/site/src/modules/auth/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_LOGIN, URL_SIGN_UP } from '@/auth/constants'; 2 | 3 | export const authRoutes = [ 4 | { 5 | path: URL_LOGIN, 6 | name: 'Login', 7 | component: () => import('@/auth/pages/LoginPage.vue'), 8 | }, 9 | { 10 | path: URL_SIGN_UP, 11 | name: 'SignUp', 12 | component: () => import('@/auth/pages/SignUpPage.vue'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/pages/ErrorPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/TheSearch.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`TheSearch > matches snapshot 1`] = `"
"`; 4 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_ORDER } from '@/order/constants'; 2 | 3 | export const orderRoutes = [ 4 | { 5 | path: URL_ORDER, 6 | name: 'OrderList', 7 | component: () => import('@/order/pages/OrderListPage.vue'), 8 | }, 9 | { 10 | path: `${URL_ORDER}/:order`, 11 | name: 'Order', 12 | component: () => import('@/order/pages/OrderPage.vue'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/pages/__snapshots__/BannerEditPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`BannerEditPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/pages/__snapshots__/CustomerPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CustomerPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-manufacturer.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/LayoutEmpty.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/pages/__snapshots__/ProductEditPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ProductEditPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/services/api.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue'; 2 | 3 | import { api } from 'mhz-helpers'; 4 | import { API_SEARCH, ISearchResults } from 'mhz-contracts'; 5 | 6 | export async function searchApi(query: Ref) { 7 | const { data } = await api.get(API_SEARCH, { params: { search: query.value } }); 8 | 9 | return data as unknown as { [key: string]: { _id: string }[] }; 10 | } 11 | -------------------------------------------------------------------------------- /apps/site/src/modules/order/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CHECKOUT, URL_PAYMENT } from '@/order/contants'; 2 | 3 | export const orderRoutes = [ 4 | { 5 | path: URL_CHECKOUT, 6 | name: 'Checkout', 7 | component: () => import('@/order/pages/CheckoutPage.vue'), 8 | }, 9 | { 10 | path: URL_PAYMENT, 11 | name: 'Payment', 12 | component: () => import('@/order/pages/PaymentPage.vue'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/PageTitle.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`PageTitle > matches snapshot 1`] = ` 4 | "
5 | 6 |

Title

7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CUSTOMER } from '@/customer/constants'; 2 | 3 | export const customerRoutes = [ 4 | { 5 | path: URL_CUSTOMER, 6 | name: 'CustomerList', 7 | component: () => import('@/customer/pages/CustomerListPage.vue'), 8 | }, 9 | { 10 | path: `${URL_CUSTOMER}/:customer`, 11 | name: 'Customer', 12 | component: () => import('@/customer/pages/CustomerPage.vue'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/pages/__snapshots__/ManufacturerEditPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManufacturerEditPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CATEGORY } from '@/category/constants'; 2 | 3 | export const categoryRoutes = [ 4 | { 5 | path: `${URL_CATEGORY}`, 6 | name: 'CategoryList', 7 | component: () => import('@/category/pages/CategoryListPage.vue'), 8 | }, 9 | { 10 | path: `${URL_CATEGORY}/:category`, 11 | name: 'Category', 12 | component: () => import('@/category/pages/CategoryPage.vue'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/pages/ProfilePage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-order.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"] 7 | }, 8 | "lint": { 9 | "outputs": [] 10 | }, 11 | "ts": { 12 | "outputs": [] 13 | }, 14 | "test": { 15 | "cache": false, 16 | "outputs": [] 17 | }, 18 | "dev": { 19 | "cache": false, 20 | "persistent": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-manager.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /apps/back/src/helpers/addView.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | import type { ICategory, IProduct, IManufacturer } from 'mhz-contracts'; 4 | 5 | export async function addView( 6 | entity: 7 | | (Document & (IProduct | ICategory | IManufacturer)) 8 | | null 9 | ) { 10 | if (!entity) return; 11 | 12 | entity.views = entity.views ? entity.views + 1 : 1; 13 | 14 | await entity.save(); 15 | } 16 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_PRODUCT } from '@/product/constants'; 2 | import { URL_CATEGORY } from '@/category/constants'; 3 | import { URL_MANUFACTURER } from '@/manufacturer/constants'; 4 | 5 | export const SEARCH_SCHEME = [ 6 | { type: 'products', labels: ['title'], url: URL_PRODUCT }, 7 | { type: 'categories', labels: ['title'], url: URL_CATEGORY }, 8 | { type: 'manufacturers', labels: ['title'], url: URL_MANUFACTURER }, 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/site/src/modules/product/components/ProductPopularList.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-product.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /apps/site/src/modules/auth/pages/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /apps/site/src/modules/cart/composables/index.ts: -------------------------------------------------------------------------------- 1 | import { computed, Ref, ComputedRef } from 'vue'; 2 | 3 | import { ICartItem } from 'mhz-contracts'; 4 | 5 | export function useCart(cart?: ComputedRef | Ref | Ref) { 6 | const count = computed(() => cart?.value?.reduce((acc, item) => acc + item.count, 0)); 7 | 8 | const price = computed(() => cart?.value?.reduce((acc, item) => acc + item.count * item.product.price, 0)); 9 | 10 | return { count, price }; 11 | } 12 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/pages/FavouritesPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /apps/site/src/modules/auth/pages/SignUpPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /apps/site/src/modules/auth/services/index.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, useMutation } from 'mhz-helpers'; 2 | import { API_AUTH_CHECK, API_AUTH_LOGIN } from 'mhz-contracts'; 3 | 4 | import { checkAuthApi, loginApi } from '@/auth/services/api'; 5 | 6 | export function checkAuth() { 7 | return useQuery({ queryKey: [API_AUTH_CHECK], queryFn: checkAuthApi }); 8 | } 9 | 10 | export function login(options: object) { 11 | return useMutation({ mutationKey: [API_AUTH_LOGIN], mutationFn: loginApi, ...options }); 12 | } 13 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/components/CategoryPopularList.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_MANUFACTURER } from '@/manufacturer/constants'; 2 | 3 | export const manufacturerRoutes = [ 4 | { 5 | path: `${URL_MANUFACTURER}`, 6 | name: 'ManufacturerList', 7 | component: () => import('@/manufacturer/pages/ManufacturerListPage.vue'), 8 | }, 9 | { 10 | path: `${URL_MANUFACTURER}/:manufacturer`, 11 | name: 'Manufacturer', 12 | component: () => import('@/manufacturer/pages/ManufacturerPage.vue'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/pages/__snapshots__/ManagerEditPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManagerEditPage > matches snapshot 1`] = ` 4 | "
5 | 6 | 7 | 8 |
" 9 | `; 10 | -------------------------------------------------------------------------------- /apps/back/src/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { buildApp, AppOptions } from './app.js'; 3 | 4 | dotenv.config({ quiet: true }); 5 | 6 | const options: AppOptions = { logger: true }; 7 | 8 | const start = async () => { 9 | const app = await buildApp(options); 10 | const port = Number(process.env.PORT); 11 | 12 | try { 13 | await app.listen({ port, host: 'localhost' }); 14 | } catch (error) { 15 | app.log.error(error); 16 | process.exit(1); 17 | } 18 | }; 19 | 20 | start(); 21 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-main.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/LayoutDefault.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`LayoutDefault > matches snapshot 1`] = ` 4 | "
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 |
" 13 | `; 14 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/TheHeader.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`TheHeader > matches snapshot 1`] = ` 4 | "
5 |
6 | 7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/site/src/modules/auth/services/api.ts: -------------------------------------------------------------------------------- 1 | import { api, setAuth } from 'mhz-helpers'; 2 | import { API_AUTH_CHECK, API_AUTH_LOGIN, ILoginData, IBaseReply, IUserToken } from 'mhz-contracts'; 3 | 4 | export async function checkAuthApi() { 5 | const { data } = await api.get(API_AUTH_CHECK); 6 | 7 | if (data.message) setAuth(true); 8 | 9 | return data; 10 | } 11 | 12 | export async function loginApi(formData: ILoginData) { 13 | const { data } = await api.post(API_AUTH_LOGIN, formData); 14 | 15 | return data; 16 | } 17 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/pages/OrderPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/components/ManufacturerPopularList.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /packages/bank/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import dts from "vite-plugin-dts"; 3 | 4 | export default defineConfig({ 5 | server: { 6 | port: 8082, 7 | }, 8 | 9 | build: { 10 | target: "es2022", 11 | copyPublicDir: false, 12 | lib: { 13 | name: "mhz-bank", 14 | entry: "./src/main.ts", 15 | formats: ["es"], 16 | }, 17 | rollupOptions: { 18 | output: { 19 | entryFileNames: `main.js`, 20 | }, 21 | }, 22 | }, 23 | 24 | plugins: [dts({ entryRoot: "./src" })], 25 | }); 26 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/pages/__snapshots__/OrderListPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`OrderListPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/pages/__snapshots__/CustomerListPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CustomerListPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/pages/MainPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /apps/back/src/models/manager.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { IManager } from 'mhz-contracts'; 4 | 5 | const managerSchema = new Schema( 6 | { 7 | firstName: { type: String }, 8 | lastName: { type: String }, 9 | password: { type: String, required: true }, 10 | email: { type: String, required: true, unique: true }, 11 | dateLoggedIn: { type: Date }, 12 | dateCreated: { type: Date, default: Date.now }, 13 | dateUpdated: { type: Date }, 14 | }, 15 | { versionKey: false } 16 | ); 17 | 18 | export default model('Manager', managerSchema); 19 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/components/__snapshots__/ImagePreview.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ImagePreview > matches snapshot 1`] = ` 4 | "
5 |
Images (sortable)
6 |
7 |
Image
8 |
9 |
" 10 | `; 11 | -------------------------------------------------------------------------------- /apps/site/src/modules/order/components/OrderList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_BANNER, URL_BANNER_CREATE, URL_BANNER_EDIT } from '@/banner/constants'; 2 | 3 | export const bannerRoutes = [ 4 | { 5 | path: URL_BANNER, 6 | name: 'BannerList', 7 | component: () => import('@/banner/pages/BannerListPage.vue'), 8 | }, 9 | { 10 | path: URL_BANNER_CREATE, 11 | name: 'BannerCreate', 12 | component: () => import('@/banner/pages/BannerCreatePage.vue'), 13 | }, 14 | { 15 | path: `${URL_BANNER_EDIT}/:banner`, 16 | name: 'BannerEdit', 17 | component: () => import('@/banner/pages/BannerEditPage.vue'), 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/pages/__snapshots__/ManagerListPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManagerListPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
Add manager 7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/pages/__snapshots__/BannerListPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`BannerListPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
Add banner 7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { useQuery, IPageQuery } from 'mhz-helpers'; 4 | import { API_CUSTOMER } from 'mhz-contracts'; 5 | 6 | import { getCustomerApi, getCustomersApi } from '@/customer/services/api'; 7 | 8 | export function getCustomers(query: Ref) { 9 | return useQuery({ queryKey: [API_CUSTOMER, query], queryFn: () => getCustomersApi(query) }); 10 | } 11 | 12 | export function getCustomer(id?: ComputedRef) { 13 | return useQuery({ queryKey: [API_CUSTOMER, id], queryFn: () => getCustomerApi(id) }); 14 | } 15 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/NavList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/components/__snapshots__/LoginForm.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`LoginForm > matches snapshot 1`] = ` 4 | "
5 | 6 |

Login

7 |
8 |
9 |
10 |
11 |
" 12 | `; 13 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_MANAGER, URL_MANAGER_CREATE, URL_MANAGER_EDIT } from '@/manager/constants'; 2 | 3 | export const managerRoutes = [ 4 | { 5 | path: URL_MANAGER, 6 | name: 'ManagerList', 7 | component: () => import('@/manager/pages/ManagerListPage.vue'), 8 | }, 9 | { 10 | path: URL_MANAGER_CREATE, 11 | name: 'ManagerCreate', 12 | component: () => import('@/manager/pages/ManagerCreatePage.vue'), 13 | }, 14 | { 15 | path: `${URL_MANAGER_EDIT}/:manager`, 16 | name: 'ManagerEdit', 17 | component: () => import('@/manager/pages/ManagerEditPage.vue'), 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_PRODUCT, URL_PRODUCT_CREATE, URL_PRODUCT_EDIT } from '@/product/constants'; 2 | 3 | export const productRoutes = [ 4 | { 5 | path: URL_PRODUCT, 6 | name: 'ProductList', 7 | component: () => import('@/product/pages/ProductListPage.vue'), 8 | }, 9 | { 10 | path: URL_PRODUCT_CREATE, 11 | name: 'ProductCreate', 12 | component: () => import('@/product/pages/ProductCreatePage.vue'), 13 | }, 14 | { 15 | path: `${URL_PRODUCT_EDIT}/:product`, 16 | name: 'ProductEdit', 17 | component: () => import('@/product/pages/ProductEditPage.vue'), 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/site/src/modules/product/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { IProductSortOption } from '@/product/interface'; 2 | 3 | export const URL_PRODUCT = '/products'; 4 | 5 | export const SORT_OPTIONS: IProductSortOption[] = [ 6 | { _id: '1', title: 'Cheap first', value: 'price', isAsc: true }, 7 | { _id: '2', title: 'Expensive first', value: 'price', isAsc: false }, 8 | { _id: '3', title: 'Newest', value: 'dateCreated', isAsc: false }, 9 | { _id: '4', title: 'Popularity', value: 'views', isAsc: false }, 10 | { _id: '5', title: 'A -> Z', value: 'title', isAsc: true }, 11 | { _id: '6', title: 'Z -> A', value: 'title', isAsc: false }, 12 | ] as const; 13 | -------------------------------------------------------------------------------- /apps/back/src/models/banner.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { IBanner } from 'mhz-contracts'; 4 | 5 | const adSchema = new Schema( 6 | { 7 | isActive: { type: Boolean, required: true }, 8 | text: { type: String, required: true }, 9 | product: { type: Schema.Types.ObjectId, ref: 'Product', required: true }, 10 | imageUrl: { type: String, required: true }, 11 | color: { type: String, required: true }, 12 | dateCreated: { type: Date, default: Date.now }, 13 | dateUpdated: { type: Date }, 14 | }, 15 | { versionKey: false } 16 | ); 17 | 18 | export default model('Banner', adSchema); 19 | -------------------------------------------------------------------------------- /apps/back/src/models/manufacturer.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { IManufacturer } from 'mhz-contracts'; 4 | 5 | const manufacturerSchema = new Schema( 6 | { 7 | title: { type: String, required: true }, 8 | description: { type: String, required: true }, 9 | logoUrl: { type: String, required: true }, 10 | country: { type: String, required: true }, 11 | views: { type: Number }, 12 | dateCreated: { type: Date, default: Date.now }, 13 | dateUpdated: { type: Date }, 14 | }, 15 | { versionKey: false } 16 | ); 17 | 18 | export default model('Manufacturer', manufacturerSchema); 19 | -------------------------------------------------------------------------------- /packages/countries/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mhz-countries", 3 | "description": "9000 Mhz countries list", 4 | "version": "1.0.73", 5 | "author": "Alexandr Dergunov (https://github.com/dergunovs)", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "sideEffects": false, 11 | "files": [ 12 | "dist/*" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/dergunovs/mhz", 17 | "directory": "packages/countries" 18 | }, 19 | "engines": { 20 | "npm": ">=11.6.2", 21 | "node": ">=25.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CATEGORY, URL_CATEGORY_CREATE, URL_CATEGORY_EDIT } from '@/category/constants'; 2 | 3 | export const categoryRoutes = [ 4 | { 5 | path: URL_CATEGORY, 6 | name: 'CategoryList', 7 | component: () => import('@/category/pages/CategoryListPage.vue'), 8 | }, 9 | { 10 | path: URL_CATEGORY_CREATE, 11 | name: 'CategoryCreate', 12 | component: () => import('@/category/pages/CategoryCreatePage.vue'), 13 | }, 14 | { 15 | path: `${URL_CATEGORY_EDIT}/:category`, 16 | name: 'CategoryEdit', 17 | component: () => import('@/category/pages/CategoryEditPage.vue'), 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/components/__snapshots__/FormButtons.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`FormButtons > matches snapshot 1`] = ` 4 | "
5 |
6 |
Confirm delete?
7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /apps/back/src/routes/stats.ts: -------------------------------------------------------------------------------- 1 | import { API_STATS_COUNT, type IEntitiesReply } from 'mhz-contracts'; 2 | 3 | import { IFastifyInstance } from '../interface/index.js'; 4 | import { countService } from '../services/stats.js'; 5 | import { statsCountSchema } from '../schemas/stats.js'; 6 | 7 | export default async function (fastify: IFastifyInstance) { 8 | fastify.get<{ Reply: { 200: IEntitiesReply } }>( 9 | API_STATS_COUNT, 10 | { preValidation: [fastify.onlyManager], ...statsCountSchema }, 11 | async function (_request, reply) { 12 | const count = await countService.count(); 13 | 14 | reply.code(200).send(count); 15 | } 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/components/ConfigurationElement.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/pages/CategoryCreatePage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import CategoryCreatePage from './CategoryCreatePage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(CategoryCreatePage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('CategoryCreatePage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(CategoryCreatePage)).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/components/PageTitle.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mhz-contracts", 3 | "description": "9000 Mhz contracts: api, types, interfaces", 4 | "version": "1.0.73", 5 | "author": "Alexandr Dergunov (https://github.com/dergunovs)", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "sideEffects": false, 11 | "files": [ 12 | "dist/*" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/dergunovs/mhz", 17 | "directory": "packages/contracts" 18 | }, 19 | "engines": { 20 | "npm": ">=11.6.2", 21 | "node": ">=25.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/services/index.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, useMutation } from 'mhz-helpers'; 2 | import { API_AUTH_CHECK, API_AUTH_LOGIN, API_AUTH_SETUP } from 'mhz-contracts'; 3 | 4 | import { checkAuthApi, loginApi, setupApi } from '@/auth/services/api'; 5 | 6 | export function checkAuth() { 7 | return useQuery({ queryKey: [API_AUTH_CHECK], queryFn: checkAuthApi }); 8 | } 9 | 10 | export function setup(options: object) { 11 | return useMutation({ mutationKey: [API_AUTH_SETUP], mutationFn: setupApi, ...options }); 12 | } 13 | 14 | export function login(options: object) { 15 | return useMutation({ mutationKey: [API_AUTH_LOGIN], mutationFn: loginApi, ...options }); 16 | } 17 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/pages/__snapshots__/ManufacturerListPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManufacturerListPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
Add manufacturer 7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/site/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createHead } from '@unhead/vue/client'; 3 | 4 | import { VueQueryPlugin, vueQueryOptions, setBaseURL } from 'mhz-helpers'; 5 | import { toast } from 'mhz-ui'; 6 | 7 | import App from '@/common/components/App.vue'; 8 | 9 | import { router } from '@/common/router'; 10 | import { TOKEN_NAME, URL_LOGIN } from '@/auth/constants'; 11 | 12 | import '@/common/styles/main.scss'; 13 | 14 | const app = createApp(App); 15 | const head = createHead(); 16 | 17 | app.use(router); 18 | app.use(head); 19 | app.use(VueQueryPlugin, vueQueryOptions(toast, URL_LOGIN, TOKEN_NAME)); 20 | 21 | setBaseURL(import.meta.env.VITE_API); 22 | 23 | app.mount('#app'); 24 | -------------------------------------------------------------------------------- /apps/admin/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createHead } from '@unhead/vue/client'; 3 | 4 | import { VueQueryPlugin, vueQueryOptions, setBaseURL } from 'mhz-helpers'; 5 | import { toast } from 'mhz-ui'; 6 | 7 | import App from '@/common/components/App.vue'; 8 | 9 | import { router } from '@/common/router'; 10 | import { TOKEN_NAME, URL_LOGIN } from '@/auth/constants'; 11 | 12 | import '@/common/styles/main.scss'; 13 | 14 | const app = createApp(App); 15 | const head = createHead(); 16 | 17 | app.use(router); 18 | app.use(head); 19 | app.use(VueQueryPlugin, vueQueryOptions(toast, URL_LOGIN, TOKEN_NAME)); 20 | 21 | setBaseURL(import.meta.env.VITE_API); 22 | 23 | app.mount('#app'); 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | 9 | # Declare files that will always have LF line endings on checkout. 10 | *.sln text eol=lf 11 | *.php text eol=lf 12 | *.js text eol=lf 13 | *.jsx text eol=lf 14 | *.ts text eol=lf 15 | *.tsx text eol=lf 16 | *.json text eol=lf 17 | *.vue text eol=lf 18 | *.css text eol=lf 19 | *.scss text eol=lf 20 | *.sass text eol=lf 21 | *.less text eol=lf 22 | 23 | # Denote all files that are truly binary and should not be modified. 24 | *.png binary 25 | *.jpg binary 26 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/pages/MainPage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import MainPage from './MainPage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(MainPage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('MainPage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(MainPage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/back/src/models/order.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { IOrder } from 'mhz-contracts'; 4 | 5 | const orderSchema = new Schema( 6 | { 7 | status: { type: String, required: true, default: 'new' }, 8 | products: [ 9 | { 10 | product: { type: Schema.Types.ObjectId, ref: 'Product' }, 11 | count: { type: Number }, 12 | }, 13 | ], 14 | customer: { type: Schema.Types.ObjectId, required: true, ref: 'Customer' }, 15 | price: { type: Number }, 16 | dateCreated: { type: Date, default: Date.now }, 17 | dateUpdated: { type: Date }, 18 | }, 19 | { versionKey: false } 20 | ); 21 | 22 | export default model('Order', orderSchema); 23 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/pages/LoginPage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import LoginPage from './LoginPage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(LoginPage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('LoginPage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(LoginPage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/pages/SetupPage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import SetupPage from './SetupPage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(SetupPage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('SetupPage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(SetupPage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/pages/ErrorPage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import ErrorPage from './ErrorPage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(ErrorPage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('ErrorPage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(ErrorPage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/services/api.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { api, IPageQuery, convertParams } from 'mhz-helpers'; 4 | import { API_CUSTOMER, ICustomer } from 'mhz-contracts'; 5 | 6 | export async function getCustomersApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: ICustomer[]; total: number }>(API_CUSTOMER, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getCustomerApi(id?: ComputedRef) { 15 | if (!id?.value) return null; 16 | 17 | const { data } = await api.get<{ data: ICustomer }>(`${API_CUSTOMER}/${id.value}`); 18 | 19 | return data.data; 20 | } 21 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/LayoutEmpty.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import LayoutEmpty from './LayoutEmpty.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(LayoutEmpty, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('LayoutEmpty', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(LayoutEmpty)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_MANUFACTURER, URL_MANUFACTURER_CREATE, URL_MANUFACTURER_EDIT } from '@/manufacturer/constants'; 2 | 3 | export const manufacturerRoutes = [ 4 | { 5 | path: URL_MANUFACTURER, 6 | name: 'ManufacturerList', 7 | component: () => import('@/manufacturer/pages/ManufacturerListPage.vue'), 8 | }, 9 | { 10 | path: URL_MANUFACTURER_CREATE, 11 | name: 'ManufacturerCreate', 12 | component: () => import('@/manufacturer/pages/ManufacturerCreatePage.vue'), 13 | }, 14 | { 15 | path: `${URL_MANUFACTURER_EDIT}/:manufacturer`, 16 | name: 'ManufacturerEdit', 17 | component: () => import('@/manufacturer/pages/ManufacturerEditPage.vue'), 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/admin/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "target": "ES2022", 6 | "lib": ["ES2022"], 7 | "module": "ESNext", 8 | "types": ["node"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /apps/site/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "target": "ES2022", 6 | "lib": ["ES2022"], 7 | "module": "ESNext", 8 | "types": ["node"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/LayoutDefault.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import LayoutDefault from './LayoutDefault.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(LayoutDefault, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('LayoutDefault', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(LayoutDefault)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/back/src/helpers/deleteFile.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | import { describe, expect, test, vi } from 'vitest'; 5 | 6 | import { deleteFile } from './deleteFile.js'; 7 | 8 | const spyUnlink = vi.spyOn(fs, 'unlinkSync').mockReturnValue(); 9 | 10 | const toFile = vi.fn(); 11 | const resize = vi.fn(() => ({ toFile })); 12 | 13 | vi.mock('sharp', () => ({ default: vi.fn(() => ({ resize })) })); 14 | 15 | describe('deleteFile', () => { 16 | test('deletes file', async () => { 17 | const filename = 'test.txt'; 18 | 19 | deleteFile(filename); 20 | 21 | expect(spyUnlink).toBeCalledTimes(1); 22 | expect(spyUnlink).toBeCalledWith(path.resolve(`./public/upload/${filename}`)); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/pages/BannerCreatePage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import BannerCreatePage from './BannerCreatePage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(BannerCreatePage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('BannerCreatePage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(BannerCreatePage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/back/src/routes/search.ts: -------------------------------------------------------------------------------- 1 | import { API_SEARCH, type ISearchResults } from 'mhz-contracts'; 2 | 3 | import { searchService } from '../services/search.js'; 4 | import { IFastifyInstance } from '../interface/index.js'; 5 | import { searchSchema } from '../schemas/search.js'; 6 | 7 | export default async function (fastify: IFastifyInstance) { 8 | fastify.get<{ Querystring: { search: string }; Reply: { 200: ISearchResults } }>( 9 | API_SEARCH, 10 | searchSchema, 11 | async function (request, reply) { 12 | const results = await searchService.search( 13 | request.query.search, 14 | fastify.jwt.decode, 15 | request.headers.authorization 16 | ); 17 | 18 | reply.code(200).send(results); 19 | } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/back/src/helpers/addView.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { Document } from 'mongoose'; 3 | 4 | import type { ICategory, IManufacturer, IProduct } from 'mhz-contracts'; 5 | 6 | import { addView } from './addView.js'; 7 | 8 | describe('addView', () => { 9 | test('adds view', async () => { 10 | const spySave = vi.fn(); 11 | const views = 1; 12 | 13 | const entity = { _id: '1', title: 'text', save: spySave, views } as unknown as 14 | | (Document & (ICategory | IProduct | IManufacturer)) 15 | | null; 16 | 17 | addView(entity); 18 | 19 | expect(spySave).toBeCalledTimes(1); 20 | expect(entity?.views).toEqual(views + 1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/site/src/modules/cart/components/CartItemList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/pages/ManagerCreatePage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import ManagerCreatePage from './ManagerCreatePage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(ManagerCreatePage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('ManagerCreatePage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(ManagerCreatePage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/pages/ProductCreatePage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import ProductCreatePage from './ProductCreatePage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(ProductCreatePage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('ProductCreatePage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(ProductCreatePage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/components/ConfigurationList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/pages/__snapshots__/ProductListPage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ProductListPage > matches snapshot 1`] = ` 4 | "
5 | 6 |
Add product 7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/services/api.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef } from 'vue'; 2 | 3 | import { api } from 'mhz-helpers'; 4 | import { API_CATEGORY, API_CATEGORY_POPULAR, ICategory } from 'mhz-contracts'; 5 | 6 | export async function getCategoriesApi() { 7 | const { data } = await api.get<{ data: ICategory[] }>(API_CATEGORY); 8 | 9 | return data.data; 10 | } 11 | 12 | export async function getCategoriesPopularApi() { 13 | const { data } = await api.get(API_CATEGORY_POPULAR); 14 | 15 | return data; 16 | } 17 | 18 | export async function getCategoryApi(id?: ComputedRef) { 19 | if (!id?.value) return null; 20 | 21 | const { data } = await api.get<{ data: ICategory }>(`${API_CATEGORY}/${id.value}`); 22 | 23 | return data.data; 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 9000 MHz 2 | 3 | Fake PC hardware store monorepo. 4 | 5 | **Frontend:** TS, Vite, Vue, VTU, Vitest, Tanstack Query. 6 | 7 | **Backend:** TS, Fastify, Mongoose, MongoDB, Swagger. 8 | 9 | **Site:** https://9000mhz.ru 10 | 11 | **Storybook:** https://ui.9000mhz.ru 12 | 13 | **Live coding:** https://youtube.com/playlist?list=PLOICX-WjKEZcwBaFQAfogv0vUvjcnr3Lj 14 | 15 | ## Installation 16 | 17 | 1. Install Node.js 25 and MongoDB 8.2 18 | 2. `npm install` - install deps from root folder 19 | 3. Create .env files in `/apps` subfolders - examples included 20 | 4. `npm run build` - build all apps and packages 21 | 5. `npm run dev` - start all apps and packages in dev mode 22 | 6. Create first manager at admin app url `/setup` 23 | 7. Swagger is availiable at backend url `/api-docs` 24 | -------------------------------------------------------------------------------- /apps/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9000 MHz - Admin 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/pages/BannerCreatePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | 3 | import { deleteAuthHeader, getCookieToken, logout } from 'mhz-helpers'; 4 | 5 | import { routes } from '@/common/router/routes'; 6 | import { TOKEN_NAME, URL_LOGIN, URL_SETUP } from '@/auth/constants'; 7 | 8 | const router = createRouter({ 9 | history: createWebHistory(), 10 | routes, 11 | scrollBehavior(_to, _from, savedPosition) { 12 | return savedPosition || { top: 0 }; 13 | }, 14 | }); 15 | 16 | router.beforeEach((to, _from, next) => { 17 | if (![URL_LOGIN, URL_SETUP].includes(to.path) && !getCookieToken(TOKEN_NAME)) { 18 | logout(URL_LOGIN, deleteAuthHeader, TOKEN_NAME); 19 | } else { 20 | next(); 21 | } 22 | }); 23 | 24 | export { router }; 25 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef } from 'vue'; 2 | 3 | import { useQuery } from 'mhz-helpers'; 4 | import { API_CATEGORY, API_CATEGORY_POPULAR } from 'mhz-contracts'; 5 | 6 | import { getCategoriesApi, getCategoriesPopularApi, getCategoryApi } from '@/category/services/api'; 7 | 8 | export function getCategories(options?: object) { 9 | return useQuery({ queryKey: [API_CATEGORY], queryFn: getCategoriesApi, ...options }); 10 | } 11 | 12 | export function getCategoriesPopular() { 13 | return useQuery({ queryKey: [API_CATEGORY_POPULAR], queryFn: getCategoriesPopularApi }); 14 | } 15 | 16 | export function getCategory(id?: ComputedRef) { 17 | return useQuery({ queryKey: [API_CATEGORY, id], queryFn: () => getCategoryApi(id) }); 18 | } 19 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/pages/ManagerCreatePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/pages/ProductCreatePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | 3 | import { deleteAuthHeader, getCookieToken, logout } from 'mhz-helpers'; 4 | 5 | import { routes } from '@/common/router/routes'; 6 | import { TOKEN_NAME, AUTH_URLS } from '@/auth/constants'; 7 | import { URL_MAIN } from '@/common/constants'; 8 | 9 | const router = createRouter({ 10 | history: createWebHistory(), 11 | routes, 12 | scrollBehavior() { 13 | document.querySelector('main')?.scrollTo(0, 0); 14 | }, 15 | }); 16 | 17 | router.beforeEach((to, _from, next) => { 18 | if (AUTH_URLS.includes(to.path) && !getCookieToken(TOKEN_NAME)) { 19 | logout(URL_MAIN, deleteAuthHeader, TOKEN_NAME); 20 | } else { 21 | next(); 22 | } 23 | }); 24 | 25 | export { router }; 26 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/services/api.ts: -------------------------------------------------------------------------------- 1 | import { api, setAuth } from 'mhz-helpers'; 2 | 3 | import { 4 | API_AUTH_CHECK, 5 | API_AUTH_LOGIN, 6 | API_AUTH_SETUP, 7 | ILoginData, 8 | ISignUpData, 9 | IBaseReply, 10 | IUserToken, 11 | } from 'mhz-contracts'; 12 | 13 | export async function checkAuthApi() { 14 | const { data } = await api.get(API_AUTH_CHECK); 15 | 16 | if (data.message) setAuth(true); 17 | 18 | return data; 19 | } 20 | export async function setupApi(formData: ISignUpData) { 21 | const { data } = await api.post(API_AUTH_SETUP, formData); 22 | 23 | return data; 24 | } 25 | 26 | export async function loginApi(formData: ILoginData) { 27 | const { data } = await api.post(API_AUTH_LOGIN, formData); 28 | 29 | return data; 30 | } 31 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/components/__snapshots__/ManagerForm.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManagerForm > matches snapshot 1`] = ` 4 | "
5 |
6 |
7 |
8 | 9 | 10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/TheSearch.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/pages/ManufacturerCreatePage.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | 4 | import ManufacturerCreatePage from './ManufacturerCreatePage.vue'; 5 | 6 | import { wrapperFactory } from '@/common/test'; 7 | 8 | let wrapper: VueWrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = wrapperFactory(ManufacturerCreatePage, {}); 12 | }); 13 | 14 | enableAutoUnmount(afterEach); 15 | 16 | describe('ManufacturerCreatePage', async () => { 17 | it('exists', async () => { 18 | expect(wrapper.findComponent(ManufacturerCreatePage)).toBeTruthy(); 19 | }); 20 | 21 | it('matches snapshot', async () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/pages/CategoryCreatePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /apps/back/src/helpers/decodeToken.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | 3 | import { decodeToken } from './decodeToken.js'; 4 | 5 | describe('decodeToken', () => { 6 | test('decodes token', async () => { 7 | const decode = vi.fn().mockImplementation((token: string) => token); 8 | const token = 'Bearer 123'; 9 | const tokenWithoutBearer = token.split('Bearer ')[1]; 10 | 11 | const decodedToken = decodeToken(decode, token); 12 | 13 | expect(decode).toBeCalledTimes(1); 14 | expect(decode).toBeCalledWith(tokenWithoutBearer); 15 | 16 | expect(decodedToken).toEqual(tokenWithoutBearer); 17 | }); 18 | 19 | test('returns null without args', async () => { 20 | const decodedToken = decodeToken(); 21 | 22 | expect(decodedToken).toEqual(null); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "lib": ["ES2022"], 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowUnreachableCode": false, 19 | "allowUnusedLabels": false, 20 | "baseUrl": "./", 21 | "rootDir": "src", 22 | "outDir": "build" 23 | }, 24 | 25 | "include": ["src"], 26 | "exclude": ["node_modules", "build", "public"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { IManager } from 'mhz-contracts'; 2 | 3 | export const MANAGERS: { data: IManager[]; total: number } = { 4 | data: [ 5 | { 6 | _id: '64f19fc5677cb758200ab511', 7 | dateCreated: '2023-09-01T08:24:37.057Z', 8 | dateUpdated: '2023-09-19T08:13:42.468Z', 9 | email: 'a@mail.ru', 10 | firstName: 'Alexandr', 11 | lastName: 'Petrov', 12 | dateLoggedIn: '2024-01-18T14:32:22.910Z', 13 | }, 14 | { 15 | _id: '64f19fc5677cb758200ab511', 16 | dateCreated: '2023-09-01T09:24:38.057Z', 17 | dateUpdated: '2023-09-19T09:13:43.468Z', 18 | email: 'a@b.ru', 19 | firstName: 'Ivan', 20 | lastName: 'Sidorov', 21 | dateLoggedIn: '2024-01-19T14:32:23.910Z', 22 | }, 23 | ], 24 | total: 1, 25 | }; 26 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/components/CustomerWatchedProducts.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /apps/admin/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, vi } from 'vitest'; 2 | import { computed } from 'vue'; 3 | import * as helpers from 'mhz-helpers'; 4 | import { UseQueryReturnType, UseMutationReturnType } from 'mhz-helpers'; 5 | 6 | beforeAll(() => { 7 | vi.spyOn(helpers, 'useValidate').mockImplementation(() => { 8 | return { 9 | error: () => undefined, 10 | isValid: () => true, 11 | errors: computed(() => undefined), 12 | }; 13 | }); 14 | 15 | vi.spyOn(helpers, 'api').mockImplementation(async () => Promise); 16 | 17 | vi.spyOn(helpers, 'useQuery').mockImplementation((value: T) => value as unknown as UseQueryReturnType); 18 | 19 | vi.spyOn(helpers, 'useMutation').mockImplementation( 20 | (value: T) => value as unknown as UseMutationReturnType 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/site/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, vi } from 'vitest'; 2 | import { computed } from 'vue'; 3 | import * as helpers from 'mhz-helpers'; 4 | import { UseQueryReturnType, UseMutationReturnType } from 'mhz-helpers'; 5 | 6 | beforeAll(() => { 7 | vi.spyOn(helpers, 'useValidate').mockImplementation(() => { 8 | return { 9 | error: () => undefined, 10 | isValid: () => true, 11 | errors: computed(() => undefined), 12 | }; 13 | }); 14 | 15 | vi.spyOn(helpers, 'api').mockImplementation(async () => Promise); 16 | 17 | vi.spyOn(helpers, 'useQuery').mockImplementation((value: T) => value as unknown as UseQueryReturnType); 18 | 19 | vi.spyOn(helpers, 'useMutation').mockImplementation( 20 | (value: T) => value as unknown as UseMutationReturnType 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/pages/ManufacturerCreatePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /apps/back/src/models/product.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { IProduct } from 'mhz-contracts'; 4 | 5 | const productSchema = new Schema( 6 | { 7 | title: { type: String, required: true }, 8 | description: { type: String, required: true }, 9 | price: { type: Number, required: true }, 10 | isInStock: { type: Boolean }, 11 | imageUrls: { type: [String], required: true }, 12 | thumbUrls: { type: [String] }, 13 | category: { type: Schema.Types.ObjectId, ref: 'Category' }, 14 | manufacturer: { type: Schema.Types.ObjectId, ref: 'Manufacturer' }, 15 | fields: { type: [Object] }, 16 | views: { type: Number }, 17 | dateCreated: { type: Date, default: Date.now }, 18 | dateUpdated: { type: Date }, 19 | }, 20 | { versionKey: false } 21 | ); 22 | 23 | export default model('Product', productSchema); 24 | -------------------------------------------------------------------------------- /apps/site/src/modules/auth/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { URL_CART } from '@/cart/constants'; 2 | import { 3 | URL_CUSTOMER, 4 | URL_CUSTOMER_CONFIGURATIONS, 5 | URL_CUSTOMER_CONFIGURATION_CREATE, 6 | URL_CUSTOMER_FAVOURITES, 7 | URL_CUSTOMER_ORDERS, 8 | URL_CUSTOMER_PROFILE, 9 | URL_CUSTOMER_WATHED_PRODUCTS, 10 | } from '@/customer/constants'; 11 | import { URL_CHECKOUT, URL_PAYMENT } from '@/order/contants'; 12 | 13 | export const URL_LOGIN = '/login'; 14 | export const URL_SIGN_UP = '/sign-up'; 15 | 16 | export const TOKEN_NAME = 'mhz_token_customer'; 17 | 18 | export const AUTH_URLS = [ 19 | URL_CUSTOMER, 20 | URL_CUSTOMER_FAVOURITES, 21 | URL_CUSTOMER_ORDERS, 22 | URL_CUSTOMER_PROFILE, 23 | URL_CUSTOMER_WATHED_PRODUCTS, 24 | URL_CUSTOMER_CONFIGURATIONS, 25 | URL_CUSTOMER_CONFIGURATION_CREATE, 26 | URL_CART, 27 | URL_CHECKOUT, 28 | URL_PAYMENT, 29 | ]; 30 | -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { useQuery, IPageQuery } from 'mhz-helpers'; 4 | import { API_MANUFACTURER, API_MANUFACTURER_POPULAR } from 'mhz-contracts'; 5 | 6 | import { getManufacturerApi, getManufacturersApi, getManufacturersPopularApi } from '@/manufacturer/services/api'; 7 | 8 | export function getManufacturers(query: Ref) { 9 | return useQuery({ queryKey: [API_MANUFACTURER, query], queryFn: () => getManufacturersApi(query) }); 10 | } 11 | 12 | export function getManufacturersPopular() { 13 | return useQuery({ queryKey: [API_MANUFACTURER_POPULAR], queryFn: getManufacturersPopularApi }); 14 | } 15 | 16 | export function getManufacturer(id?: ComputedRef) { 17 | return useQuery({ queryKey: [API_MANUFACTURER, id], queryFn: () => getManufacturerApi(id) }); 18 | } 19 | -------------------------------------------------------------------------------- /apps/back/src/models/category.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { ICategory } from 'mhz-contracts'; 4 | 5 | const categorySchema = new Schema( 6 | { 7 | title: { type: String, required: true }, 8 | description: { type: String, required: true }, 9 | iconUrl: { type: String, required: true }, 10 | fields: { 11 | type: [ 12 | { 13 | title: { type: String, required: true }, 14 | fieldType: { type: String, required: true }, 15 | fieldValue: { type: Schema.Types.Mixed, required: true }, 16 | fieldUnits: { type: String }, 17 | }, 18 | ], 19 | }, 20 | views: { type: Number }, 21 | dateCreated: { type: Date, default: Date.now }, 22 | dateUpdated: { type: Date }, 23 | }, 24 | { versionKey: false } 25 | ); 26 | 27 | export default model('Category', categorySchema); 28 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/admin/src/modules/auth/components/__snapshots__/SetupForm.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`SetupForm > matches snapshot 1`] = ` 4 | "
5 | 6 |

Add first manager

7 |
8 |
9 |
10 |
11 |
12 |
13 |
" 14 | `; 15 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/login.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/components/__snapshots__/CategoryFieldForm.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CategoryFieldForm > matches snapshot 1`] = ` 4 | "
5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | 13 |
14 |
" 15 | `; 16 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CUSTOMER = '/customer'; 2 | export const URL_CUSTOMER_FAVOURITES = `${URL_CUSTOMER}/favourites`; 3 | export const URL_CUSTOMER_WATHED_PRODUCTS = `${URL_CUSTOMER}/watched`; 4 | export const URL_CUSTOMER_ORDERS = `${URL_CUSTOMER}/orders`; 5 | export const URL_CUSTOMER_PROFILE = `${URL_CUSTOMER}/profile`; 6 | export const URL_CUSTOMER_CONFIGURATIONS = `${URL_CUSTOMER}/configurations`; 7 | export const URL_CUSTOMER_CONFIGURATION_CREATE = `${URL_CUSTOMER}/createConfiguration`; 8 | 9 | export const CUSTOMER_NAV = [ 10 | { _id: '1', url: URL_CUSTOMER_ORDERS, title: 'Orders' }, 11 | { _id: '2', url: URL_CUSTOMER_CONFIGURATIONS, title: 'PC configurations' }, 12 | { _id: '3', url: URL_CUSTOMER_FAVOURITES, title: 'Favourites' }, 13 | { _id: '4', url: URL_CUSTOMER_WATHED_PRODUCTS, title: 'Watched products' }, 14 | { _id: '5', url: URL_CUSTOMER_PROFILE, title: 'Profile' }, 15 | ]; 16 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/bank/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mhz-bank", 3 | "description": "9000 Mhz fake bank app", 4 | "version": "1.0.73", 5 | "author": "Alexandr Dergunov (https://github.com/dergunovs)", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/main.js", 9 | "types": "dist/main.d.ts", 10 | "sideEffects": false, 11 | "files": [ 12 | "dist/*" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/dergunovs/mhz", 17 | "directory": "packages/bank" 18 | }, 19 | "scripts": { 20 | "build": "tsc && vite build", 21 | "dev": "vite", 22 | "ts": "tsc --noEmit" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "25.0.1", 26 | "prettier": "3.7.4", 27 | "typescript": "5.9.3", 28 | "vite": "7.2.7", 29 | "vite-plugin-dts": "4.5.4" 30 | }, 31 | "engines": { 32 | "npm": ">=11.6.2", 33 | "node": ">=25.2.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/__snapshots__/NavList.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`NavList > matches snapshot 1`] = ` 4 | "" 14 | `; 15 | -------------------------------------------------------------------------------- /packages/bank/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowUnreachableCode": false, 19 | "allowUnusedLabels": false, 20 | "baseUrl": "./", 21 | "paths": { "@/*": ["src/*"] }, 22 | "types": ["vite/client", "node"], 23 | "noEmit": true 24 | }, 25 | "include": ["src/**/*.ts", "src/*.d.ts"], 26 | "exclude": ["node_modules", "dist"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/services/api.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { api, IPageQuery, convertParams } from 'mhz-helpers'; 4 | import { API_MANUFACTURER, API_MANUFACTURER_POPULAR, IManufacturer } from 'mhz-contracts'; 5 | 6 | export async function getManufacturersApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: IManufacturer[]; total: number }>(API_MANUFACTURER, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getManufacturersPopularApi() { 15 | const { data } = await api.get(API_MANUFACTURER_POPULAR); 16 | 17 | return data; 18 | } 19 | 20 | export async function getManufacturerApi(id?: ComputedRef) { 21 | if (!id?.value) return null; 22 | 23 | const { data } = await api.get<{ data: IManufacturer }>(`${API_MANUFACTURER}/${id.value}`); 24 | 25 | return data.data; 26 | } 27 | -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/components/ManufacturerCatalogList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 38 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { useQuery, useMutation, IPageQuery } from 'mhz-helpers'; 4 | import { API_ORDER } from 'mhz-contracts'; 5 | 6 | import { deleteOrderApi, getOrderApi, getOrdersApi, updateOrderApi } from '@/order/services/api'; 7 | 8 | export function getOrders(query: Ref) { 9 | return useQuery({ queryKey: [API_ORDER, query], queryFn: () => getOrdersApi(query) }); 10 | } 11 | 12 | export function getOrder(id?: ComputedRef) { 13 | return useQuery({ queryKey: [API_ORDER, id], queryFn: () => getOrderApi(id) }); 14 | } 15 | 16 | export function updateOrder(id: ComputedRef, options: object) { 17 | return useMutation({ mutationKey: [API_ORDER, id], mutationFn: updateOrderApi, ...options }); 18 | } 19 | 20 | export function deleteOrder(options: object) { 21 | return useMutation({ mutationKey: [API_ORDER], mutationFn: deleteOrderApi, ...options }); 22 | } 23 | -------------------------------------------------------------------------------- /apps/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowUnreachableCode": false, 19 | "allowUnusedLabels": false, 20 | "baseUrl": "./", 21 | "paths": { "@/*": ["src/modules/*"] }, 22 | "types": ["vite/client", "vite-svg-loader", "node"], 23 | "noEmit": true 24 | }, 25 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], 26 | "exclude": ["node_modules", "dist"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const URL_CONFIGURATION = '/configurations'; 2 | 3 | export const CONFIGURATION_CATEGORIES_ORDER = [ 4 | 'Motherboard', 5 | 'CPU', 6 | 'Cooler', 7 | 'Case', 8 | 'GPU', 9 | 'PSU', 10 | 'RAM', 11 | 'SSD', 12 | 'Monitor', 13 | 'Keyboard', 14 | 'Mouse', 15 | 'Mousepad', 16 | ]; 17 | 18 | export const CONFIGURATION_PRODUCT_FIELDS: { [key: string]: string[] } = { 19 | Motherboard: ['Format', 'Socket', 'RAM type'], 20 | CPU: ['TDP', 'Socket', 'Cores'], 21 | Cooler: ['Height', 'Dissipated power', 'Heat pipes'], 22 | Case: ['Format', 'Max cooler height', 'Color'], 23 | GPU: ['Memory', 'Recommended power supply'], 24 | PSU: ['Power', 'Certificate 80 plus'], 25 | RAM: ['Size', 'Type'], 26 | SSD: ['Sequential read', 'Sequential write'], 27 | Monitor: ['Resolution', 'Panel type'], 28 | Keyboard: ['Keys type', 'Wireless'], 29 | Mouse: ['Sensitivity', 'Wireless'], 30 | Mousepad: ['Width', 'Length'], 31 | }; 32 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/pages/OrdersPage.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /apps/site/src/modules/order/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { IPageQuery, useMutation, useQuery } from 'mhz-helpers'; 4 | import { API_ORDER } from 'mhz-contracts'; 5 | 6 | import { getOrderApi, getOrdersApi, postOrderApi, updateOrderApi } from '@/order/services/api'; 7 | 8 | export function getOrders(query: Ref) { 9 | return useQuery({ queryKey: [API_ORDER, query], queryFn: () => getOrdersApi(query) }); 10 | } 11 | 12 | export function getOrder(id?: ComputedRef) { 13 | return useQuery({ queryKey: [API_ORDER, id], queryFn: () => getOrderApi(id) }); 14 | } 15 | 16 | export function updateOrder(id: ComputedRef, options: object) { 17 | return useMutation({ mutationKey: [API_ORDER, id], mutationFn: updateOrderApi, ...options }); 18 | } 19 | 20 | export function postOrder(options: object) { 21 | return useMutation({ mutationKey: [API_ORDER], mutationFn: postOrderApi, ...options }); 22 | } 23 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/PageTitle.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 38 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/components/__snapshots__/ManagerList.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ManagerList > matches snapshot 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
a@mail.ruAlexandr Petrov1 сент. 2023 г.19 сент. 2023 г.
a@b.ruIvan Sidorov1 сент. 2023 г.19 сент. 2023 г.
" 20 | `; 21 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/components/__snapshots__/CustomerList.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CustomerList > matches snapshot 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
a@b.ruAlexandr Ivanot10 сент. 2023 г.11 сент. 2023 г.
test@mail.ruAlex Petrov11 февр. 2023 г.12 февр. 2023 г.
" 20 | `; 21 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/NavList.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { VueWrapper, enableAutoUnmount } from '@vue/test-utils'; 3 | import { dataTest } from 'mhz-helpers'; 4 | 5 | import NavList from './NavList.vue'; 6 | 7 | import { wrapperFactory } from '@/common/test'; 8 | import { NAV_ITEMS } from '@/layout/constants'; 9 | 10 | const navListItem = dataTest('nav-list-item'); 11 | 12 | let wrapper: VueWrapper; 13 | 14 | beforeEach(() => { 15 | wrapper = wrapperFactory(NavList, {}); 16 | }); 17 | 18 | enableAutoUnmount(afterEach); 19 | 20 | describe('NavList', async () => { 21 | it('exists', async () => { 22 | expect(wrapper.findComponent(NavList)).toBeTruthy(); 23 | }); 24 | 25 | it('matches snapshot', async () => { 26 | expect(wrapper.html()).toMatchSnapshot(); 27 | }); 28 | 29 | it('shows nav list items', async () => { 30 | expect(wrapper.findAllComponents(navListItem).length).toEqual(NAV_ITEMS.length); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { IManufacturer } from 'mhz-contracts'; 2 | 3 | export const MANUFACTURERS: { data: IManufacturer[]; total: number } = { 4 | data: [ 5 | { 6 | _id: '64dd1a67f8fd2bdaa35cc735', 7 | dateCreated: '2023-08-16T18:50:15.242Z', 8 | dateUpdated: '2023-09-11T16:26:24.516Z', 9 | title: 'Apacer', 10 | country: 'China', 11 | logoUrl: 'resized-1692211814147-Apacer.png', 12 | views: 15, 13 | }, 14 | { 15 | _id: '64dd1999f8fd2bdaa35cc723', 16 | dateCreated: '2023-08-16T18:46:49.184Z', 17 | title: 'Palit', 18 | country: 'China', 19 | logoUrl: 'resized-1692211607919-Palit_logo.png', 20 | views: 2, 21 | }, 22 | { 23 | _id: '64dd1907f8fd2bdaa35cc711', 24 | dateCreated: '2023-08-16T18:44:23.306Z', 25 | title: 'Samsung', 26 | country: 'Korea', 27 | logoUrl: 'resized-1692211461686-samsung.png', 28 | views: 1, 29 | }, 30 | ], 31 | total: 1, 32 | }; 33 | -------------------------------------------------------------------------------- /apps/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowUnreachableCode": false, 19 | "allowUnusedLabels": false, 20 | "baseUrl": "./", 21 | "paths": { "@/*": ["src/modules/*"] }, 22 | "types": ["vite/client", "vite-svg-loader", "node", "vite-plugin-pwa/vue"], 23 | "noEmit": true 24 | }, 25 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], 26 | "exclude": ["node_modules", "dist"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/NavItem.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 40 | -------------------------------------------------------------------------------- /apps/back/src/helpers/addProductToWatched.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | 3 | import Customer from '../models/customer.js'; 4 | 5 | export async function addProductToWatched( 6 | userId: string | Schema.Types.ObjectId, 7 | productId: string | Schema.Types.ObjectId 8 | ) { 9 | const filter = { _id: userId }; 10 | const limit = 8; 11 | 12 | const currentCustomer = await Customer.findOne(filter).exec(); 13 | 14 | const watchedProductsIds = currentCustomer?.watchedProducts?.map((watched) => watched.product._id?.toString()) || []; 15 | 16 | if (watchedProductsIds.includes(productId.toString())) return; 17 | 18 | if (currentCustomer?.watchedProducts) { 19 | if (currentCustomer.watchedProducts.length === limit) { 20 | await Customer.updateOne(filter, { $pop: { watchedProducts: -1 } }); 21 | } 22 | 23 | await Customer.updateOne(filter, { 24 | $push: { watchedProducts: { product: productId, dateCreated: new Date() } }, 25 | }); 26 | } 27 | 28 | await currentCustomer?.save(); 29 | } 30 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/components/CategoryCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 | 48 | -------------------------------------------------------------------------------- /apps/back/src/models/customer.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | import type { ICustomer } from 'mhz-contracts'; 4 | 5 | const customerSchema = new Schema( 6 | { 7 | firstName: { type: String }, 8 | lastName: { type: String }, 9 | password: { type: String, required: true }, 10 | email: { type: String, required: true, unique: true }, 11 | cart: [{ product: { type: Schema.Types.ObjectId, ref: 'Product' }, count: { type: Number } }], 12 | orders: [{ type: Schema.Types.ObjectId, ref: 'Order' }], 13 | watchedProducts: [{ product: { type: Schema.Types.ObjectId, ref: 'Product' }, dateCreated: { type: Date } }], 14 | favouriteProducts: [{ type: Schema.Types.ObjectId, ref: 'Product' }], 15 | configurations: [{ type: Schema.Types.ObjectId, ref: 'Configuration' }], 16 | dateLoggedIn: { type: Date }, 17 | dateCreated: { type: Date, default: Date.now }, 18 | dateUpdated: { type: Date }, 19 | }, 20 | { versionKey: false } 21 | ); 22 | 23 | export default model('Customer', customerSchema); 24 | -------------------------------------------------------------------------------- /apps/back/eslint.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import js from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import pluginImportX from 'eslint-plugin-import-x'; 6 | import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 7 | import pluginSonar from 'eslint-plugin-sonarjs'; 8 | import pluginUnicorn from 'eslint-plugin-unicorn'; 9 | import globals from 'globals'; 10 | import { parser, options, ignores, settings, rules } from 'fastify-linters-config'; 11 | 12 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 13 | 14 | export default tseslint.config( 15 | js.configs.recommended, 16 | ...tseslint.configs.recommended, 17 | pluginSonar.configs.recommended, 18 | pluginUnicorn.configs.recommended, 19 | pluginImportX.flatConfigs.recommended, 20 | pluginImportX.flatConfigs.typescript, 21 | 22 | ignores, 23 | 24 | parser(tseslint.parser, dirname), 25 | 26 | { ...options(globals), ...settings, ...rules }, 27 | 28 | pluginPrettierRecommended 29 | ); 30 | -------------------------------------------------------------------------------- /apps/site/src/modules/customer/components/CustomerNav.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 47 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/pages/CategoryListPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/back/src/plugins/swagger.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import swagger from '@fastify/swagger'; 3 | import swaggerUi from '@fastify/swagger-ui'; 4 | 5 | import dotenv from 'dotenv'; 6 | 7 | dotenv.config({ quiet: true }); 8 | 9 | export default fp(async function (fastify) { 10 | fastify.register(swagger, { 11 | swagger: { 12 | info: { title: '9000 Mhz swagger', version: '1.0.12' }, 13 | externalDocs: { url: 'https://github.com/dergunovs/mhz', description: '9000 Mhz on Github' }, 14 | host: 'localhost:5000', 15 | schemes: ['http'], 16 | consumes: ['application/json'], 17 | produces: ['application/json'], 18 | securityDefinitions: { 19 | token: { type: 'apiKey', name: 'Authorization', in: 'header' }, 20 | }, 21 | }, 22 | }); 23 | 24 | if (process.env.IS_DEV) { 25 | fastify.register(swaggerUi, { 26 | routePrefix: '/api-docs', 27 | uiConfig: { 28 | docExpansion: 'list', 29 | defaultModelsExpandDepth: 0, 30 | tryItOutEnabled: true, 31 | }, 32 | }); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /apps/back/src/helpers/createThumb.spec.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | 3 | import { describe, expect, test, vi } from 'vitest'; 4 | 5 | import { createThumb } from './createThumb.js'; 6 | 7 | const toFile = vi.fn(); 8 | const webp = vi.fn(() => ({ toFile })); 9 | const resize = vi.fn(() => ({ webp })); 10 | 11 | vi.mock('sharp', () => ({ default: vi.fn(() => ({ resize })) })); 12 | 13 | describe('createThumb', () => { 14 | test('resizes file', async () => { 15 | const filename = 'test.txt'; 16 | 17 | const resizedFilename = await createThumb(filename); 18 | 19 | expect(sharp).toBeCalledTimes(1); 20 | expect(sharp).toBeCalledWith(`./public/upload/${filename}`); 21 | 22 | expect(resize).toBeCalledTimes(1); 23 | expect(resize).toBeCalledWith(480); 24 | 25 | expect(webp).toBeCalledTimes(1); 26 | expect(webp).toBeCalledWith({ quality: 64 }); 27 | 28 | expect(toFile).toBeCalledTimes(1); 29 | expect(toFile).toBeCalledWith(`./public/upload/thumb-${filename}.webp`); 30 | 31 | expect(resizedFilename).toEqual(`thumb-${filename}.webp`); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/components/ConfigurationAuthor.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 41 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/components/__snapshots__/EntitiesCount.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`EntitiesCount > matches snapshot 1`] = ` 4 | "
5 |
6 | 7 | 8 |
9 | 10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/catalog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/filters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue'; 2 | 3 | import { useQuery, useMutation } from 'mhz-helpers'; 4 | import { API_UPLOAD, API_SEARCH, API_STATS_COUNT, API_UPLOAD_MULTIPLE } from 'mhz-contracts'; 5 | 6 | import { deleteFileApi, getEntitiesCountApi, searchApi, uploadFileApi, uploadFilesApi } from '@/common/services/api'; 7 | 8 | export function search(query: Ref) { 9 | return useQuery({ queryKey: [API_SEARCH, query], queryFn: () => searchApi(query), enabled: false }); 10 | } 11 | 12 | export function getEntitiesCount() { 13 | return useQuery({ queryKey: [API_STATS_COUNT], queryFn: getEntitiesCountApi }); 14 | } 15 | 16 | export function uploadFile(options: object) { 17 | return useMutation({ mutationKey: [API_UPLOAD], mutationFn: uploadFileApi, ...options }); 18 | } 19 | 20 | export function uploadFiles(options: object) { 21 | return useMutation({ mutationKey: [API_UPLOAD_MULTIPLE], mutationFn: uploadFilesApi, ...options }); 22 | } 23 | 24 | export function deleteFile() { 25 | return useMutation({ mutationKey: [API_UPLOAD], mutationFn: deleteFileApi }); 26 | } 27 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/pages/OrderPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | 3 | import { authRoutes } from '@/auth/routes'; 4 | import { categoryRoutes } from '@/category/routes'; 5 | import { manufacturerRoutes } from '@/manufacturer/routes'; 6 | import { orderRoutes } from '@/order/routes'; 7 | import { productRoutes } from '@/product/routes'; 8 | import { managerRoutes } from '@/manager/routes'; 9 | import { customerRoutes } from '@/customer/routes'; 10 | import { bannerRoutes } from '@/banner/routes'; 11 | 12 | import { URL_MAIN, URL_ERROR } from '@/common/constants'; 13 | 14 | export const routes: RouteRecordRaw[] = [ 15 | ...authRoutes, 16 | ...categoryRoutes, 17 | ...manufacturerRoutes, 18 | ...orderRoutes, 19 | ...productRoutes, 20 | ...managerRoutes, 21 | ...customerRoutes, 22 | ...bannerRoutes, 23 | 24 | { path: URL_MAIN, name: 'Main', component: () => import('@/common/pages/MainPage.vue') }, 25 | 26 | { path: URL_ERROR, name: '404', component: () => import('@/common/pages/ErrorPage.vue'), meta: { layout: 'empty' } }, 27 | { path: '/:catchAll(.*)', name: 'error', redirect: '404' }, 28 | ]; 29 | -------------------------------------------------------------------------------- /apps/site/src/modules/order/components/OrderElement.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | 29 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aleksandr Dergunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { useQuery, useMutation, IPageQuery } from 'mhz-helpers'; 4 | import { API_BANNER } from 'mhz-contracts'; 5 | 6 | import { deleteBannerApi, getBannerApi, getBannersApi, postBannerApi, updateBannerApi } from '@/banner/services/api'; 7 | 8 | export function getBanners(query: Ref) { 9 | return useQuery({ queryKey: [API_BANNER, query], queryFn: () => getBannersApi(query) }); 10 | } 11 | 12 | export function getBanner(id?: ComputedRef) { 13 | return useQuery({ queryKey: [API_BANNER, id], queryFn: () => getBannerApi(id) }); 14 | } 15 | 16 | export function postBanner(options: object) { 17 | return useMutation({ mutationKey: [API_BANNER], mutationFn: postBannerApi, ...options }); 18 | } 19 | 20 | export function updateBanner(options: object) { 21 | return useMutation({ mutationKey: [API_BANNER], mutationFn: updateBannerApi, ...options }); 22 | } 23 | 24 | export function deleteBanner(options: object) { 25 | return useMutation({ mutationKey: [API_BANNER], mutationFn: deleteBannerApi, ...options }); 26 | } 27 | -------------------------------------------------------------------------------- /apps/site/src/modules/order/services/api.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { IPageQuery, api, convertParams } from 'mhz-helpers'; 4 | import { API_ORDER, IBaseReply, IOrder, TOrderStatus } from 'mhz-contracts'; 5 | 6 | export async function getOrdersApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: IOrder[]; total: number }>(API_ORDER, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getOrderApi(id?: ComputedRef) { 15 | if (!id?.value) return null; 16 | 17 | const { data } = await api.get<{ data: IOrder }>(`${API_ORDER}/${id.value}`); 18 | 19 | return data.data; 20 | } 21 | 22 | export async function updateOrderApi(order: { status: TOrderStatus; id?: string }) { 23 | if (!order.id) return null; 24 | 25 | const { data } = await api.patch(`${API_ORDER}/${order.id}`, { status: order.status }); 26 | 27 | return data; 28 | } 29 | 30 | export async function postOrderApi() { 31 | const { data } = await api.post(API_ORDER); 32 | 33 | return data; 34 | } 35 | -------------------------------------------------------------------------------- /apps/site/src/modules/product/components/ProductAddToCartButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/pages/BannerEditPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/components/__snapshots__/ProductFieldsForm.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ProductFieldsForm > matches snapshot 1`] = ` 4 | "
5 |

Specs

6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
" 20 | `; 21 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/pages/ProductEditPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/components/TheSearch.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | 33 | 46 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/services/api.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef } from 'vue'; 2 | 3 | import { api } from 'mhz-helpers'; 4 | import { API_CATEGORY, ICategory, IBaseReply } from 'mhz-contracts'; 5 | 6 | export async function getCategoriesApi() { 7 | const { data } = await api.get<{ data: ICategory[] }>(API_CATEGORY); 8 | 9 | return data.data; 10 | } 11 | 12 | export async function getCategoryApi(id?: ComputedRef) { 13 | if (!id?.value) return null; 14 | 15 | const { data } = await api.get<{ data: ICategory }>(`${API_CATEGORY}/${id.value}`); 16 | 17 | return data.data; 18 | } 19 | 20 | export async function postCategoryApi(formData: ICategory) { 21 | const { data } = await api.post(API_CATEGORY, formData); 22 | 23 | return data; 24 | } 25 | 26 | export async function updateCategoryApi(formData: ICategory) { 27 | const { data } = await api.patch(`${API_CATEGORY}/${formData._id}`, formData); 28 | 29 | return data; 30 | } 31 | 32 | export async function deleteCategoryApi(id?: string) { 33 | if (!id) return null; 34 | 35 | const { data } = await api.delete(`${API_CATEGORY}/${id}`); 36 | 37 | return data; 38 | } 39 | -------------------------------------------------------------------------------- /apps/site/src/modules/cart/components/CartHeaderButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 44 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/pages/CategoryEditPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/pages/CategoryListPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 44 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef } from 'vue'; 2 | 3 | import { useQuery, useMutation } from 'mhz-helpers'; 4 | import { API_CATEGORY } from 'mhz-contracts'; 5 | 6 | import { 7 | deleteCategoryApi, 8 | getCategoriesApi, 9 | getCategoryApi, 10 | postCategoryApi, 11 | updateCategoryApi, 12 | } from '@/category/services/api'; 13 | 14 | export function getCategories(options?: object) { 15 | return useQuery({ queryKey: [API_CATEGORY], queryFn: getCategoriesApi, ...options }); 16 | } 17 | 18 | export function getCategory(id?: ComputedRef) { 19 | return useQuery({ queryKey: [API_CATEGORY, id], queryFn: () => getCategoryApi(id) }); 20 | } 21 | 22 | export function postCategory(options: object) { 23 | return useMutation({ mutationKey: [API_CATEGORY], mutationFn: postCategoryApi, ...options }); 24 | } 25 | 26 | export function updateCategory(options: object) { 27 | return useMutation({ mutationKey: [API_CATEGORY], mutationFn: updateCategoryApi, ...options }); 28 | } 29 | 30 | export function deleteCategory(options: object) { 31 | return useMutation({ mutationKey: [API_CATEGORY], mutationFn: deleteCategoryApi, ...options }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/components/__snapshots__/OrderList.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`OrderList > matches snapshot 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
65940c82cc2989495f96d911352000 ₽Alexandr Dergunovnew2 янв. 2024 г.2 янв. 2024 г.
6587ec081614096a5773abdb29900 ₽Alexandr Dergunovcompleted24 дек. 2023 г.30 дек. 2023 г.
" 24 | `; 25 | -------------------------------------------------------------------------------- /apps/admin/src/modules/order/services/api.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { api, IPageQuery, convertParams } from 'mhz-helpers'; 4 | import { API_ORDER, IBaseReply, IOrder, TOrderStatus } from 'mhz-contracts'; 5 | 6 | export async function getOrdersApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: IOrder[]; total: number }>(API_ORDER, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getOrderApi(id?: ComputedRef) { 15 | if (!id?.value) return null; 16 | 17 | const { data } = await api.get<{ data: IOrder }>(`${API_ORDER}/${id.value}`); 18 | 19 | return data.data; 20 | } 21 | 22 | export async function updateOrderApi(order: { status: TOrderStatus; id?: string }) { 23 | if (!order.id) return null; 24 | 25 | const { data } = await api.patch(`${API_ORDER}/${order.id}`, { status: order.status }); 26 | 27 | return data; 28 | } 29 | 30 | export async function deleteOrderApi(id?: string) { 31 | if (!id) return null; 32 | 33 | const { data } = await api.delete(`${API_ORDER}/${id}`); 34 | 35 | return data; 36 | } 37 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/components/LayoutDefault.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 22 | 52 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { useMutation, useQuery, IPageQuery } from 'mhz-helpers'; 4 | import { API_MANAGER } from 'mhz-contracts'; 5 | 6 | import { 7 | deleteManagerApi, 8 | getManagerApi, 9 | getManagersApi, 10 | postManagerApi, 11 | updateManagerApi, 12 | } from '@/manager/services/api'; 13 | 14 | export function getManagers(query: Ref) { 15 | return useQuery({ queryKey: [API_MANAGER, query], queryFn: () => getManagersApi(query) }); 16 | } 17 | 18 | export function getManager(id?: ComputedRef) { 19 | return useQuery({ queryKey: [API_MANAGER, id], queryFn: () => getManagerApi(id) }); 20 | } 21 | 22 | export function postManager(options: object) { 23 | return useMutation({ mutationKey: [API_MANAGER], mutationFn: postManagerApi, ...options }); 24 | } 25 | 26 | export function updateManager(options: object) { 27 | return useMutation({ mutationKey: [API_MANAGER], mutationFn: updateManagerApi, ...options }); 28 | } 29 | 30 | export function deleteManager(options: object) { 31 | return useMutation({ mutationKey: [API_MANAGER], mutationFn: deleteManagerApi, ...options }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { useQuery, useMutation, IPageQuery } from 'mhz-helpers'; 4 | import { API_PRODUCT } from 'mhz-contracts'; 5 | 6 | import { 7 | getProductsApi, 8 | getProductApi, 9 | postProductApi, 10 | updateProductApi, 11 | deleteProductApi, 12 | } from '@/product/services/api'; 13 | 14 | export function getProducts(query: Ref) { 15 | return useQuery({ queryKey: [API_PRODUCT, query], queryFn: () => getProductsApi(query) }); 16 | } 17 | 18 | export function getProduct(id?: ComputedRef) { 19 | return useQuery({ queryKey: [API_PRODUCT, id], queryFn: () => getProductApi(id) }); 20 | } 21 | 22 | export function postProduct(options: object) { 23 | return useMutation({ mutationKey: [API_PRODUCT], mutationFn: postProductApi, ...options }); 24 | } 25 | 26 | export function updateProduct(options: object) { 27 | return useMutation({ mutationKey: [API_PRODUCT], mutationFn: updateProductApi, ...options }); 28 | } 29 | 30 | export function deleteProduct(options: object) { 31 | return useMutation({ mutationKey: [API_PRODUCT], mutationFn: deleteProductApi, ...options }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/site/src/modules/manufacturer/components/ManufacturerCard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 52 | -------------------------------------------------------------------------------- /apps/admin/eslint.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import js from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import pluginVue from 'eslint-plugin-vue'; 6 | import vueParser from 'vue-eslint-parser'; 7 | import pluginImportX from 'eslint-plugin-import-x'; 8 | import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 9 | import pluginSonar from 'eslint-plugin-sonarjs'; 10 | import pluginUnicorn from 'eslint-plugin-unicorn'; 11 | import globals from 'globals'; 12 | import { parser, options, ignores, settings, rules } from 'vue-linters-config'; 13 | 14 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 15 | 16 | export default tseslint.config( 17 | js.configs.recommended, 18 | ...tseslint.configs.recommended, 19 | ...pluginVue.configs['flat/strongly-recommended'], 20 | pluginImportX.flatConfigs.recommended, 21 | pluginSonar.configs.recommended, 22 | pluginUnicorn.configs.recommended, 23 | pluginImportX.flatConfigs.typescript, 24 | 25 | ignores, 26 | 27 | parser(vueParser, tseslint.parser, dirname), 28 | 29 | { ...options(globals), ...settings, ...rules }, 30 | 31 | pluginPrettierRecommended 32 | ); 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-category.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /apps/back/src/app.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import Fastify, { FastifyServerOptions } from 'fastify'; 4 | import autoload from '@fastify/autoload'; 5 | import { Schema, connect } from 'mongoose'; 6 | import dotenv from 'dotenv'; 7 | 8 | import { addSchemas } from './schemas/addSchemas.js'; 9 | 10 | dotenv.config({ quiet: true }); 11 | 12 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 13 | 14 | Schema.Types.Boolean.convertToFalse.add(''); 15 | connect(`mongodb://127.0.0.1/${process.env.DATABASE}`); 16 | 17 | type AppOptions = Partial; 18 | 19 | async function buildApp(options: AppOptions = {}) { 20 | const fastify = Fastify(options); 21 | 22 | fastify.register(autoload, { dir: path.join(dirname, 'plugins'), options: { ...options } }); 23 | fastify.register(autoload, { dir: path.join(dirname, 'routes'), options: { ...options, prefix: '/api' } }); 24 | 25 | fastify.setErrorHandler(function (_error, _request, reply) { 26 | reply.status(500).send({ message: 'Server error' }); 27 | }); 28 | 29 | addSchemas(fastify); 30 | 31 | return fastify; 32 | } 33 | 34 | export { buildApp }; 35 | export type { AppOptions }; 36 | -------------------------------------------------------------------------------- /apps/site/eslint.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import js from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import pluginVue from 'eslint-plugin-vue'; 6 | import vueParser from 'vue-eslint-parser'; 7 | import pluginImportX from 'eslint-plugin-import-x'; 8 | import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 9 | import pluginSonar from 'eslint-plugin-sonarjs'; 10 | import pluginUnicorn from 'eslint-plugin-unicorn'; 11 | import globals from 'globals'; 12 | import { parser, options, ignores, settings, rules } from 'vue-linters-config'; 13 | 14 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 15 | 16 | export default tseslint.config( 17 | js.configs.recommended, 18 | ...tseslint.configs.recommended, 19 | ...pluginVue.configs['flat/strongly-recommended'], 20 | pluginImportX.flatConfigs.recommended, 21 | pluginSonar.configs.recommended, 22 | pluginUnicorn.configs.recommended, 23 | pluginImportX.flatConfigs.typescript, 24 | 25 | ignores, 26 | 27 | parser(vueParser, tseslint.parser, dirname), 28 | 29 | { ...options(globals), ...settings, ...rules }, 30 | 31 | pluginPrettierRecommended 32 | ); 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/customer/pages/CustomerPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /apps/back/src/helpers/resizeFile.spec.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | 3 | import { describe, expect, test, vi } from 'vitest'; 4 | 5 | import { resizeFile } from './resizeFile.js'; 6 | import * as helpers from './deleteFile.js'; 7 | 8 | const toFile = vi.fn(); 9 | const resize = vi.fn(() => ({ toFile })); 10 | 11 | vi.mock('sharp', () => ({ default: vi.fn(() => ({ resize })) })); 12 | 13 | const spyDeleteFile = vi.spyOn(helpers, 'deleteFile').mockReturnValue(); 14 | 15 | describe('resizeFile', () => { 16 | test('resizes file', async () => { 17 | const filename = 'test.txt'; 18 | const width = '500'; 19 | 20 | const resizedFilename = await resizeFile(filename, width); 21 | 22 | expect(sharp).toBeCalledTimes(1); 23 | expect(sharp).toBeCalledWith(`./public/upload/${filename}`); 24 | 25 | expect(resize).toBeCalledTimes(1); 26 | expect(resize).toBeCalledWith(Number(width)); 27 | 28 | expect(toFile).toBeCalledTimes(1); 29 | expect(toFile).toBeCalledWith(`./public/upload/resized-${filename}`); 30 | 31 | expect(spyDeleteFile).toBeCalledTimes(1); 32 | expect(spyDeleteFile).toBeCalledWith(filename); 33 | 34 | expect(resizedFilename).toEqual(`resized-${filename}`); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/components/LayoutDefault.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 60 | -------------------------------------------------------------------------------- /apps/site/src/modules/cart/pages/CartPage.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 52 | -------------------------------------------------------------------------------- /apps/site/src/modules/category/components/CategoryCatalogList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 55 | -------------------------------------------------------------------------------- /apps/site/src/modules/common/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | 3 | import { URL_MAIN, URL_PRIVACY, URL_ERROR } from '@/common/constants'; 4 | 5 | import { authRoutes } from '@/auth/routes'; 6 | import { categoryRoutes } from '@/category/routes'; 7 | import { manufacturerRoutes } from '@/manufacturer/routes'; 8 | import { productRoutes } from '@/product/routes'; 9 | import { customerRoutes } from '@/customer/routes'; 10 | import { cartRoutes } from '@/cart/routes'; 11 | import { orderRoutes } from '@/order/routes'; 12 | import { configurationRoutes } from '@/configuration/routes'; 13 | 14 | export const routes: RouteRecordRaw[] = [ 15 | ...cartRoutes, 16 | ...authRoutes, 17 | ...categoryRoutes, 18 | ...manufacturerRoutes, 19 | ...productRoutes, 20 | ...customerRoutes, 21 | ...orderRoutes, 22 | ...configurationRoutes, 23 | 24 | { path: URL_MAIN, name: 'Main', component: () => import('@/common/pages/MainPage.vue') }, 25 | { path: URL_PRIVACY, name: 'Privacy', component: () => import('@/common/pages/PrivacyPage.vue') }, 26 | 27 | { path: URL_ERROR, name: '404', component: () => import('@/common/pages/ErrorPage.vue'), meta: { layout: 'empty' } }, 28 | { path: '/:catchAll(.*)', name: 'error', redirect: '404' }, 29 | ]; 30 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { IEntitiesReply } from 'mhz-contracts'; 2 | 3 | export const COUNT: IEntitiesReply = { 4 | base: { 5 | labels: ['Categories', 'Manufacturers', 'Managers', 'Customers', 'Orders'], 6 | datasets: [{ data: [12, 20, 1, 1, 12] }], 7 | }, 8 | categories: { 9 | labels: [ 10 | 'Mousepad', 11 | 'Monitor', 12 | 'CPU', 13 | 'Keyboard', 14 | 'PSU', 15 | 'Cooler', 16 | 'Case', 17 | 'Mouse', 18 | 'Motherboard', 19 | 'GPU', 20 | 'SSD', 21 | 'RAM', 22 | ], 23 | datasets: [{ data: [13, 20, 15, 16, 15, 12, 16, 13, 20, 20, 20, 20] }], 24 | }, 25 | manufacturers: { 26 | labels: [ 27 | 'MSI', 28 | 'ASRock', 29 | 'be quiet!', 30 | 'HyperX', 31 | 'Samsung', 32 | 'Fractal Design', 33 | 'Lian Li', 34 | 'Zalman', 35 | 'G.Skill', 36 | 'AMD', 37 | 'Thermaltake', 38 | 'Logitech', 39 | 'ASUS', 40 | 'GIGABYTE', 41 | 'Intel', 42 | 'Deepcool', 43 | 'Razer', 44 | 'Palit', 45 | 'Western Digital', 46 | 'Apacer', 47 | ], 48 | datasets: [{ data: [18, 4, 6, 7, 12, 3, 2, 7, 7, 11, 13, 13, 24, 27, 8, 12, 9, 5, 4, 8] }], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/icons/signup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/pages/ManufacturerEditPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | -------------------------------------------------------------------------------- /apps/site/src/modules/layout/components/FullscreenFilters.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /apps/back/src/services/manager.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import type { IManager, IQuery } from 'mhz-contracts'; 3 | 4 | import Manager from '../models/manager.js'; 5 | 6 | import { paginate } from '../helpers/index.js'; 7 | import { IBaseService } from '../interface/index.js'; 8 | 9 | export const managerService: IBaseService = { 10 | getMany: async (query?: IQuery) => { 11 | const { data, total } = await paginate(Manager, query); 12 | 13 | return { data: data as T[], total }; 14 | }, 15 | 16 | getOne: async (_id: string) => { 17 | const manager: IManager | null = await Manager.findOne({ _id }).lean().exec(); 18 | 19 | return { data: manager as T }; 20 | }, 21 | 22 | update: async (itemToUpdate: T, _id?: string) => { 23 | await Manager.findOneAndUpdate({ _id }, { ...itemToUpdate, dateUpdated: new Date() }); 24 | }, 25 | 26 | create: async (managerToCreate: T) => { 27 | const manager = new Manager(managerToCreate); 28 | 29 | if (!manager.password) return; 30 | 31 | manager.password = await bcrypt.hash(manager.password, 10); 32 | 33 | await manager.save(); 34 | }, 35 | 36 | delete: async (_id?: string) => { 37 | const manager = await Manager.findOne({ _id }); 38 | 39 | await manager?.deleteOne(); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /apps/admin/src/modules/common/components/EntitiesCount.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 37 | 38 | 50 | -------------------------------------------------------------------------------- /apps/admin/src/modules/layout/icons/nav-customer.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manager/services/api.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { api, IPageQuery, convertParams } from 'mhz-helpers'; 4 | import { API_MANAGER, IBaseReply, IManager } from 'mhz-contracts'; 5 | 6 | export async function getManagersApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: IManager[]; total: number }>(API_MANAGER, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getManagerApi(id?: ComputedRef) { 15 | if (!id?.value) return null; 16 | 17 | const { data } = await api.get<{ data: IManager }>(`${API_MANAGER}/${id.value}`); 18 | 19 | return data.data; 20 | } 21 | 22 | export async function postManagerApi(formData: IManager) { 23 | const { data } = await api.post(API_MANAGER, formData); 24 | 25 | return data; 26 | } 27 | 28 | export async function updateManagerApi(formData: IManager) { 29 | const { data } = await api.patch(`${API_MANAGER}/${formData._id}`, formData); 30 | 31 | return data; 32 | } 33 | 34 | export async function deleteManagerApi(id?: string) { 35 | if (!id) return null; 36 | 37 | const { data } = await api.delete(`${API_MANAGER}/${id}`); 38 | 39 | return data; 40 | } 41 | -------------------------------------------------------------------------------- /apps/site/src/modules/configuration/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue'; 2 | 3 | import { IPageQuery, useMutation, useQuery } from 'mhz-helpers'; 4 | import { API_CONFIGURATION } from 'mhz-contracts'; 5 | 6 | import { 7 | deleteConfigurationApi, 8 | getConfigurationApi, 9 | getConfigurationsApi, 10 | postConfigurationApi, 11 | updateConfigurationApi, 12 | } from '@/configuration/services/api'; 13 | 14 | export function getConfigurations(query: Ref) { 15 | return useQuery({ queryKey: [API_CONFIGURATION, query], queryFn: () => getConfigurationsApi(query) }); 16 | } 17 | 18 | export function getConfiguration(id?: string | string[]) { 19 | return useQuery({ queryKey: [API_CONFIGURATION, id], queryFn: () => getConfigurationApi(id) }); 20 | } 21 | 22 | export function updateConfiguration(options: object) { 23 | return useMutation({ mutationKey: [API_CONFIGURATION], mutationFn: updateConfigurationApi, ...options }); 24 | } 25 | 26 | export function postConfiguration(options: object) { 27 | return useMutation({ mutationKey: [API_CONFIGURATION], mutationFn: postConfigurationApi, ...options }); 28 | } 29 | 30 | export function deleteConfiguration(options: object) { 31 | return useMutation({ mutationKey: [API_CONFIGURATION], mutationFn: deleteConfigurationApi, ...options }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/admin/src/modules/banner/services/api.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { IPageQuery, api, convertParams } from 'mhz-helpers'; 4 | import { API_BANNER, IBanner, IBaseReply, IFilterData } from 'mhz-contracts'; 5 | 6 | export async function getBannersApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: IBanner[]; total: number; filters: IFilterData }>(API_BANNER, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getBannerApi(id?: ComputedRef) { 15 | if (!id?.value) return null; 16 | 17 | const { data } = await api.get<{ data: IBanner }>(`${API_BANNER}/${id.value}`); 18 | 19 | return data.data; 20 | } 21 | 22 | export async function postBannerApi(formData: IBanner) { 23 | const { data } = await api.post(API_BANNER, formData); 24 | 25 | return data; 26 | } 27 | 28 | export async function updateBannerApi(formData: IBanner) { 29 | const { data } = await api.patch(`${API_BANNER}/${formData._id}`, formData); 30 | 31 | return data; 32 | } 33 | 34 | export async function deleteBannerApi(id?: string) { 35 | if (!id) return null; 36 | 37 | const { data } = await api.delete(`${API_BANNER}/${id}`); 38 | 39 | return data; 40 | } 41 | -------------------------------------------------------------------------------- /apps/admin/src/modules/manufacturer/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref } from 'vue'; 2 | 3 | import { useMutation, useQuery, IPageQuery } from 'mhz-helpers'; 4 | import { API_MANUFACTURER } from 'mhz-contracts'; 5 | 6 | import { 7 | deleteManufacturerApi, 8 | getManufacturerApi, 9 | getManufacturersApi, 10 | postManufacturerApi, 11 | updateManufacturerApi, 12 | } from '@/manufacturer/services/api'; 13 | 14 | export function getManufacturers(query: Ref) { 15 | return useQuery({ queryKey: [API_MANUFACTURER, query], queryFn: () => getManufacturersApi(query) }); 16 | } 17 | 18 | export function getManufacturer(id?: ComputedRef) { 19 | return useQuery({ queryKey: [API_MANUFACTURER, id], queryFn: () => getManufacturerApi(id) }); 20 | } 21 | 22 | export function postManufacturer(options: object) { 23 | return useMutation({ mutationKey: [API_MANUFACTURER], mutationFn: postManufacturerApi, ...options }); 24 | } 25 | 26 | export function updateManufacturer(options: object) { 27 | return useMutation({ mutationKey: [API_MANUFACTURER], mutationFn: updateManufacturerApi, ...options }); 28 | } 29 | 30 | export function deleteManufacturer(options: object) { 31 | return useMutation({ mutationKey: [API_MANUFACTURER], mutationFn: deleteManufacturerApi, ...options }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/site/src/modules/cart/components/CartItemCount.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 57 | -------------------------------------------------------------------------------- /apps/admin/src/modules/category/components/CategoryList.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | -------------------------------------------------------------------------------- /apps/admin/src/modules/product/services/api.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ComputedRef } from 'vue'; 2 | 3 | import { api, IPageQuery, convertParams } from 'mhz-helpers'; 4 | import { API_PRODUCT, IProduct, IFilterData, IBaseReply } from 'mhz-contracts'; 5 | 6 | export async function getProductsApi(query: Ref) { 7 | const params = convertParams(query); 8 | 9 | const { data } = await api.get<{ data: IProduct[]; total: number; filters: IFilterData }>(API_PRODUCT, { params }); 10 | 11 | return data; 12 | } 13 | 14 | export async function getProductApi(id?: ComputedRef) { 15 | if (!id?.value) return null; 16 | 17 | const { data } = await api.get<{ data: IProduct }>(`${API_PRODUCT}/${id.value}`); 18 | 19 | return data.data; 20 | } 21 | 22 | export async function postProductApi(formData: IProduct) { 23 | const { data } = await api.post(API_PRODUCT, formData); 24 | 25 | return data; 26 | } 27 | 28 | export async function updateProductApi(formData: IProduct) { 29 | const { data } = await api.patch(`${API_PRODUCT}/${formData._id}`, formData); 30 | 31 | return data; 32 | } 33 | 34 | export async function deleteProductApi(id?: string) { 35 | if (!id) return null; 36 | 37 | const { data } = await api.delete(`${API_PRODUCT}/${id}`); 38 | 39 | return data; 40 | } 41 | --------------------------------------------------------------------------------