├── .prettierignore ├── app ├── client │ ├── .env.local │ ├── .env.production │ ├── page-components │ │ ├── index.ts │ │ └── TopPageComponent │ │ │ ├── TopPageComponent.props.ts │ │ │ ├── TopPageComponent.module.css │ │ │ ├── sort.reducer.ts │ │ │ └── TopPageComponent.tsx │ ├── public │ │ └── favicon.ico │ ├── layout │ │ ├── Sidebar │ │ │ ├── Sidebar.module.css │ │ │ ├── Sidebar.props.ts │ │ │ └── Sidebar.tsx │ │ ├── Layout.props.ts │ │ ├── Footer │ │ │ ├── Footer.props.ts │ │ │ ├── Footer.module.css │ │ │ └── Footer.tsx │ │ ├── Header │ │ │ ├── Header.props.ts │ │ │ ├── Header.module.css │ │ │ └── Header.tsx │ │ ├── Layout.module.css │ │ ├── Menu │ │ │ ├── Menu.module.css │ │ │ └── Menu.tsx │ │ ├── Layout.tsx │ │ └── logo.svg │ ├── components │ │ ├── HhData │ │ │ ├── HhData.props.ts │ │ │ ├── rate.svg │ │ │ ├── HhData.module.css │ │ │ └── HhData.tsx │ │ ├── Htag │ │ │ ├── Htag.props.ts │ │ │ ├── Htag.module.css │ │ │ └── Htag.tsx │ │ ├── Advantages │ │ │ ├── Advantages.props.ts │ │ │ ├── Advantages.module.css │ │ │ ├── Advantages.tsx │ │ │ └── check.svg │ │ ├── Card │ │ │ ├── Card.module.css │ │ │ ├── Card.props.ts │ │ │ └── Card.tsx │ │ ├── Divider │ │ │ ├── Divider.module.css │ │ │ ├── Divider.props.ts │ │ │ └── Divider.tsx │ │ ├── Search │ │ │ ├── Search.props.ts │ │ │ ├── Search.module.css │ │ │ ├── glass.svg │ │ │ └── Search.tsx │ │ ├── ReviewForm │ │ │ ├── ReviewForm.interface.ts │ │ │ ├── ReviewForm.props.ts │ │ │ ├── close.svg │ │ │ ├── ReviewForm.module.css │ │ │ └── ReviewForm.tsx │ │ ├── P │ │ │ ├── P.module.css │ │ │ ├── P.props.ts │ │ │ └── P.tsx │ │ ├── Sort │ │ │ ├── sort.svg │ │ │ ├── Sort.props.ts │ │ │ ├── Sort.module.css │ │ │ └── Sort.tsx │ │ ├── ButtonIcon │ │ │ ├── menu.svg │ │ │ ├── close.svg │ │ │ ├── up.svg │ │ │ ├── ButtonIcon.props.ts │ │ │ ├── ButtonIcon.tsx │ │ │ └── ButtonIcon.module.css │ │ ├── Review │ │ │ ├── Review.props.ts │ │ │ ├── Review.module.css │ │ │ ├── user.svg │ │ │ └── Review.tsx │ │ ├── Product │ │ │ ├── Product.props.ts │ │ │ ├── Product.module.css │ │ │ └── Product.tsx │ │ ├── Input │ │ │ ├── Input.props.ts │ │ │ ├── Input.module.css │ │ │ └── Input.tsx │ │ ├── TextArea │ │ │ ├── TextArea.props.ts │ │ │ ├── TextArea.module.css │ │ │ └── TextArea.tsx │ │ ├── Tag │ │ │ ├── Tag.props.ts │ │ │ ├── Tag.tsx │ │ │ └── Tag.module.css │ │ ├── Up │ │ │ ├── Up.module.css │ │ │ └── Up.tsx │ │ ├── Rating │ │ │ ├── Rating.props.ts │ │ │ ├── Rating.module.css │ │ │ ├── star.svg │ │ │ └── Rating.tsx │ │ ├── Button │ │ │ ├── Button.props.ts │ │ │ ├── arrow.svg │ │ │ ├── Button.module.css │ │ │ └── Button.tsx │ │ └── index.ts │ ├── .stylelintrc.json │ ├── next-env.d.ts │ ├── docker-compose.yaml │ ├── Dockerfile │ ├── pages │ │ ├── 500.tsx │ │ ├── 404.tsx │ │ ├── search.tsx │ │ ├── _app.tsx │ │ ├── [type] │ │ │ ├── index.tsx │ │ │ └── [alias].tsx │ │ └── index.tsx │ ├── next.config.js │ ├── helpers │ │ ├── api.ts │ │ ├── icons │ │ │ ├── services.svg │ │ │ ├── courses.svg │ │ │ ├── products.svg │ │ │ └── books.svg │ │ └── helpers.tsx │ ├── interfaces │ │ ├── menu.interface.ts │ │ ├── product.interface.ts │ │ └── page.interface.ts │ ├── .gitignore │ ├── .eslintrc │ ├── hooks │ │ └── useScrollY.ts │ ├── tsconfig.json │ ├── styles │ │ └── globals.css │ ├── context │ │ └── app.context.tsx │ └── package.json └── server │ ├── .prettierrc │ ├── nest-cli.json │ ├── src │ ├── review │ │ ├── review.constants.ts │ │ ├── dto │ │ │ └── create-review.dto.ts │ │ ├── review.model.ts │ │ ├── review.module.ts │ │ ├── review.service.ts │ │ ├── review.service.spec.ts │ │ └── review.controller.ts │ ├── pipes │ │ ├── id-validation.constants.ts │ │ └── id-validation.pipe.ts │ ├── telegram │ │ ├── telegram.constants.ts │ │ ├── telegram.interface.ts │ │ ├── telegram.service.ts │ │ └── telegram.module.ts │ ├── product │ │ ├── product.constants.ts │ │ ├── dto │ │ │ ├── find.product.dto.ts │ │ │ └── create-roduct.dto.ts │ │ ├── product.module.ts │ │ ├── product.model.ts │ │ ├── product.controller.ts │ │ └── product.service.ts │ ├── files │ │ ├── dto │ │ │ └── file-element.response.ts │ │ ├── mfile.class.ts │ │ ├── files.module.ts │ │ ├── files.service.ts │ │ └── files.controller.ts │ ├── top-page │ │ ├── top-page.constants.ts │ │ ├── dto │ │ │ ├── find-top-page.dto.ts │ │ │ └── create-top-page.dto.ts │ │ ├── top-page.module.ts │ │ ├── top-page.model.ts │ │ ├── top-page.service.ts │ │ └── top-page.controller.ts │ ├── hh │ │ ├── hh.controller.ts │ │ ├── hh.constants.ts │ │ ├── hh.module.ts │ │ ├── hh.service.ts │ │ └── hh.models.ts │ ├── auth │ │ ├── guards │ │ │ └── jwt.guard.ts │ │ ├── dto │ │ │ └── auth.dto.ts │ │ ├── auth.constants.ts │ │ ├── user.model.ts │ │ ├── strategies │ │ │ └── jwt.strategy.ts │ │ ├── auth.module.ts │ │ ├── auth.controller.ts │ │ └── auth.service.ts │ ├── main.ts │ ├── decorators │ │ └── user-email.decorator.ts │ ├── configs │ │ ├── jwt.config.ts │ │ ├── telegram.config.ts │ │ └── mongo.config.ts │ └── app.module.ts │ ├── tsconfig.build.json │ ├── Dockerfile │ ├── .env │ ├── test │ ├── jest-e2e.json │ ├── auth.e2e-spec.ts │ └── review.e2e-spec.ts │ ├── .dockerignore │ ├── .gitignore │ ├── tsconfig.json │ ├── docker-compose.yaml │ ├── .eslintrc.js │ └── package.json ├── .prettierrc ├── img ├── pic-course01-p01.png ├── pic-course02-m07-p01.png ├── pic-course02-m07-p02.png ├── pic-course02-m07-p03.png ├── pic-course02-m07-p04.png ├── pic-course02-m07-p05.png ├── pic-course02-m08-p01.png ├── pic-course02-m08-p02.png ├── pic-course02-m09-p01.png ├── pic-course02-m10-p01.png ├── pic-course02-m10-p02.png ├── pic-course02-m11-p01.png ├── pic-course02-m12-p01.png ├── pic-course02-m12-p02.png ├── pic-course02-m13-p01.png ├── pic-course02-m13-p02.png ├── pic-course02-m14-p01.png ├── pic-course02-m14-p02.png ├── pic-course02-m14-p03.png ├── pic-course02-m14-p04.png ├── pic-course02-m15-p01.png ├── pic-course02-m15-p02.png ├── pic-course02-m15-p03.png ├── pic-course02-m15-p04.png ├── pic-course02-m15-p05.png ├── pic-course02-m16-p01.png ├── pic-course02-m16-p02.png ├── pic-course02-m16-p03.png ├── pic-course02-m17-p01.png └── pic-course02-m17-p02.png ├── .gitignore ├── course-files └── figma │ └── Курс 2 - NextJS.fig ├── .github └── workflows │ ├── client-push-images-to-github.yml.disabled │ └── server-push-images-to-github.yml.disabled ├── Readme.md ├── 02-Client-Development.md └── 01-Server-Development.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # *.test.js 3 | # *.spec.js -------------------------------------------------------------------------------- /app/client/.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DOMAIN=https://courses-top.ru -------------------------------------------------------------------------------- /app/client/.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DOMAIN=https://courses-top.ru -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": true 4 | } 5 | -------------------------------------------------------------------------------- /app/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /app/client/page-components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TopPageComponent/TopPageComponent'; 2 | -------------------------------------------------------------------------------- /img/pic-course01-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course01-p01.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.~ 2 | *.log 3 | node_modules/ 4 | package-lock.json 5 | yarn.lock 6 | mongo-data-4.4 7 | -------------------------------------------------------------------------------- /app/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/app/client/public/favicon.ico -------------------------------------------------------------------------------- /app/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /img/pic-course02-m07-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m07-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m07-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m07-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m07-p03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m07-p03.png -------------------------------------------------------------------------------- /img/pic-course02-m07-p04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m07-p04.png -------------------------------------------------------------------------------- /img/pic-course02-m07-p05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m07-p05.png -------------------------------------------------------------------------------- /img/pic-course02-m08-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m08-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m08-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m08-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m09-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m09-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m10-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m10-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m10-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m10-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m11-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m11-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m12-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m12-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m12-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m12-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m13-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m13-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m13-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m13-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m14-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m14-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m14-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m14-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m14-p03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m14-p03.png -------------------------------------------------------------------------------- /img/pic-course02-m14-p04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m14-p04.png -------------------------------------------------------------------------------- /img/pic-course02-m15-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m15-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m15-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m15-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m15-p03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m15-p03.png -------------------------------------------------------------------------------- /img/pic-course02-m15-p04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m15-p04.png -------------------------------------------------------------------------------- /img/pic-course02-m15-p05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m15-p05.png -------------------------------------------------------------------------------- /img/pic-course02-m16-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m16-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m16-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m16-p02.png -------------------------------------------------------------------------------- /img/pic-course02-m16-p03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m16-p03.png -------------------------------------------------------------------------------- /img/pic-course02-m17-p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m17-p01.png -------------------------------------------------------------------------------- /img/pic-course02-m17-p02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/img/pic-course02-m17-p02.png -------------------------------------------------------------------------------- /app/server/src/review/review.constants.ts: -------------------------------------------------------------------------------- 1 | export const REVIEW_NOT_FOUND = '[App] Отзыв с таким id не найден'; 2 | -------------------------------------------------------------------------------- /app/server/src/pipes/id-validation.constants.ts: -------------------------------------------------------------------------------- 1 | export const ID_VALIDATION_ERROR = '[App] Неверный формат Id'; 2 | -------------------------------------------------------------------------------- /app/server/src/telegram/telegram.constants.ts: -------------------------------------------------------------------------------- 1 | export const TELEGRAM_MODULE_OPTIONS = 'TELEGRAM_MODULE_OPTIONS'; 2 | -------------------------------------------------------------------------------- /app/server/src/product/product.constants.ts: -------------------------------------------------------------------------------- 1 | export const PRODUCT_NOT_FOUND_ERROR = '[App] Продукт с таким ID не найден!'; 2 | -------------------------------------------------------------------------------- /course-files/figma/Курс 2 - NextJS.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/app/HEAD/course-files/figma/Курс 2 - NextJS.fig -------------------------------------------------------------------------------- /app/client/layout/Sidebar/Sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | display: grid; 3 | align-content: start; 4 | gap: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /app/server/src/files/dto/file-element.response.ts: -------------------------------------------------------------------------------- 1 | export class FileElementResponse { 2 | url: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /app/server/src/top-page/top-page.constants.ts: -------------------------------------------------------------------------------- 1 | export const NOT_FOUND_TOP_PAGE_ERROR = '[App] Страница с таким Id не найдена!'; 2 | -------------------------------------------------------------------------------- /app/server/src/hh/hh.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | @Controller('hh') 4 | export class HhController {} 5 | -------------------------------------------------------------------------------- /app/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /app/client/layout/Layout.props.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export interface ILayoutProps { 4 | children: ReactNode; 5 | } 6 | -------------------------------------------------------------------------------- /app/server/src/auth/guards/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | 3 | export class JwtAuthGuard extends AuthGuard('jwt') {} 4 | -------------------------------------------------------------------------------- /app/client/components/HhData/HhData.props.ts: -------------------------------------------------------------------------------- 1 | import { IHhData } from 'interfaces/page.interface'; 2 | 3 | export interface IHhDataProps extends IHhData {} 4 | -------------------------------------------------------------------------------- /app/client/components/Htag/Htag.props.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export interface IHtagProps { 4 | tag: 'h1' | 'h2' | 'h3'; 5 | children: ReactNode; 6 | } 7 | -------------------------------------------------------------------------------- /app/client/components/Advantages/Advantages.props.ts: -------------------------------------------------------------------------------- 1 | import { ITopPageAdvantage } from 'interfaces/page.interface'; 2 | 3 | export interface IAdvantagesProps { 4 | advantages: ITopPageAdvantage[]; 5 | } 6 | -------------------------------------------------------------------------------- /app/server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | WORKDIR /app 3 | ADD ./package.json ./package.json 4 | RUN npm install 5 | ADD ./ ./ 6 | RUN npm run build 7 | RUN npm prune --production 8 | CMD ["node", "./dist/main.js"] -------------------------------------------------------------------------------- /app/server/src/auth/dto/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class AuthDto { 4 | @IsString() 5 | login: string; 6 | 7 | @IsString() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /app/client/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-order-config-standard"], 3 | "plugins": ["stylelint-order"], 4 | "rules": { 5 | "color-hex-case": "upper" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/components/Card/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | border-radius: 5px; 3 | background: var(--white); 4 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05); 5 | } 6 | 7 | .blue { 8 | background: #F9F8FF; 9 | } 10 | -------------------------------------------------------------------------------- /app/client/layout/Footer/Footer.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export interface IFooterProps 4 | extends DetailedHTMLProps, HTMLDivElement> {} 5 | -------------------------------------------------------------------------------- /app/client/layout/Header/Header.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export interface IHeaderProps 4 | extends DetailedHTMLProps, HTMLDivElement> {} 5 | -------------------------------------------------------------------------------- /app/client/layout/Sidebar/Sidebar.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export interface ISidebarProps 4 | extends DetailedHTMLProps, HTMLDivElement> {} 5 | -------------------------------------------------------------------------------- /app/client/components/Divider/Divider.module.css: -------------------------------------------------------------------------------- 1 | .hr { 2 | width: 100%; 3 | height: 1px; 4 | 5 | margin-top: 20px; 6 | margin-bottom: 20px; 7 | 8 | border: none; 9 | background: var(--gray-light); 10 | } 11 | -------------------------------------------------------------------------------- /app/client/components/Divider/Divider.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export interface IDividerProps 4 | extends DetailedHTMLProps, HTMLHRElement> {} 5 | -------------------------------------------------------------------------------- /app/client/components/Search/Search.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export interface ISearchProps 4 | extends DetailedHTMLProps, HTMLFormElement> {} 5 | -------------------------------------------------------------------------------- /app/server/.env: -------------------------------------------------------------------------------- 1 | MONGO_DATABASE_NAME=admin 2 | MONGO_LOGIN=admin 3 | MONGO_PASSWORD=admin 4 | MONGO_HOST=localhost 5 | MONGO_PORT=27017 6 | JWT_SECRET=MY_JWT_SECRET 7 | TELEGRAM_TOKEN=TELEGRAM_TOKEN 8 | CHAT_ID=CHAT_ID 9 | HH_TOKEN=HH_TOKEN -------------------------------------------------------------------------------- /app/client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.svg' { 5 | const content: React.FC>; 6 | export default content; 7 | } 8 | -------------------------------------------------------------------------------- /app/server/src/product/dto/find.product.dto.ts: -------------------------------------------------------------------------------- 1 | import {IsNumber, IsString} from 'class-validator'; 2 | 3 | export class FindProductDto { 4 | @IsString() 5 | category: string; 6 | 7 | @IsNumber() 8 | limit: number; 9 | } 10 | -------------------------------------------------------------------------------- /app/client/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | image: docker.pkg.github.com/AlariCode/top-app-demo/top-app-demo:develop 5 | container_name: top-app-demo 6 | restart: always 7 | ports: 8 | - 3000:3000 9 | -------------------------------------------------------------------------------- /app/server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | WORKDIR /opt/app 3 | 4 | ADD ./package.json ./package.json 5 | RUN npm install 6 | ADD ./ ./ 7 | ENV NODE_ENV production 8 | RUN npm run build 9 | RUN npm prune --rpoduction 10 | CMD ["npm", "start"] 11 | EXPOSE 3000 -------------------------------------------------------------------------------- /app/client/components/ReviewForm/ReviewForm.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IReviewForm { 2 | name: string; 3 | title: string; 4 | description: string; 5 | rating: number; 6 | } 7 | 8 | export interface IReviewSentResponse { 9 | message: string; 10 | } 11 | -------------------------------------------------------------------------------- /app/server/src/files/mfile.class.ts: -------------------------------------------------------------------------------- 1 | export class MFile { 2 | originalname: string; 3 | buffer: Buffer; 4 | 5 | constructor(file: Express.Multer.File | MFile) { 6 | this.originalname = file.originalname; 7 | this.buffer = file.buffer; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/server/src/top-page/dto/find-top-page.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum } from 'class-validator'; 2 | import { ETopLevelCategory } from 'top-page/top-page.model'; 3 | 4 | export class FindTopPageDto { 5 | @IsEnum(ETopLevelCategory) 6 | firstCategory: ETopLevelCategory; 7 | } 8 | -------------------------------------------------------------------------------- /app/client/components/P/P.module.css: -------------------------------------------------------------------------------- 1 | .p { 2 | margin: 0; 3 | } 4 | 5 | .s { 6 | font-size: 14px; 7 | line-height: 24px; 8 | } 9 | 10 | .m { 11 | font-size: 16px; 12 | line-height: 24px; 13 | } 14 | 15 | .l { 16 | font-size: 18px; 17 | line-height: 29px; 18 | } 19 | -------------------------------------------------------------------------------- /app/client/components/Card/Card.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'; 2 | 3 | export interface ICardProps 4 | extends DetailedHTMLProps, HTMLDivElement> { 5 | color?: 'white' | 'blue'; 6 | children: ReactNode; 7 | } 8 | -------------------------------------------------------------------------------- /app/client/components/ReviewForm/ReviewForm.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export interface IReviewFormProps 4 | extends DetailedHTMLProps, HTMLDivElement> { 5 | productId: string; 6 | isOpened: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /app/server/src/hh/hh.constants.ts: -------------------------------------------------------------------------------- 1 | const API_ROOT_API = 'https://api.hh.ru'; 2 | 3 | export const API_URL = { 4 | vacancies: API_ROOT_API + '/vacancies', 5 | }; 6 | 7 | export const SALARY_CLUSTER_ID = 'salary'; 8 | 9 | export const CLUSTER_FIND_ERROR = 'Не найден кластер Salary'; 10 | -------------------------------------------------------------------------------- /app/client/components/Sort/sort.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | app.setGlobalPrefix('api'); 7 | await app.listen(3000); 8 | } 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /app/client/pages/500.tsx: -------------------------------------------------------------------------------- 1 | import { Htag } from 'components'; 2 | import { withLayout } from 'layout/Layout'; 3 | 4 | function Error500(): JSX.Element { 5 | return ( 6 | <> 7 | Ошибка 500 8 | 9 | ); 10 | } 11 | 12 | export default withLayout(Error500); 13 | -------------------------------------------------------------------------------- /app/server/src/auth/auth.constants.ts: -------------------------------------------------------------------------------- 1 | export const ALREADY_REGISTERED_ERROR = 2 | '[App] Такой пользователь уже был зарегистрирован!'; 3 | 4 | export const USER_NOT_FOUND_ERROR = 5 | '[App] Пользователь с таким email не найден!'; 6 | 7 | export const WRONG_PASSWORD_ERROR = '[App] Неверный пароль!'; 8 | -------------------------------------------------------------------------------- /app/client/components/ButtonIcon/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/ReviewForm/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Htag } from 'components'; 2 | import { withLayout } from 'layout/Layout'; 3 | 4 | export function Error404(): JSX.Element { 5 | return ( 6 | <> 7 | Ошибка 404 8 | 9 | ); 10 | } 11 | 12 | export default withLayout(Error404); 13 | -------------------------------------------------------------------------------- /app/client/components/Review/Review.props.ts: -------------------------------------------------------------------------------- 1 | import { IReviewModel } from 'interfaces/product.interface'; 2 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 3 | 4 | export interface IReviewProps 5 | extends DetailedHTMLProps, HTMLDivElement> { 6 | review: IReviewModel; 7 | } 8 | -------------------------------------------------------------------------------- /app/client/components/Product/Product.props.ts: -------------------------------------------------------------------------------- 1 | import { IProductModel } from 'interfaces/product.interface'; 2 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 3 | 4 | export interface IProductProps 5 | extends DetailedHTMLProps, HTMLDivElement> { 6 | product: IProductModel; 7 | } 8 | -------------------------------------------------------------------------------- /app/client/components/ButtonIcon/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/P/P.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'; 2 | 3 | export interface IPProps 4 | extends DetailedHTMLProps< 5 | HTMLAttributes, 6 | HTMLParagraphElement 7 | > { 8 | size?: 's' | 'm' | 'l'; 9 | children: ReactNode; 10 | } 11 | -------------------------------------------------------------------------------- /app/client/components/Search/Search.module.css: -------------------------------------------------------------------------------- 1 | .search { 2 | position: relative; 3 | 4 | width: 100%; 5 | } 6 | 7 | .input input { 8 | width: 100%; 9 | } 10 | 11 | .button { 12 | position: absolute; 13 | top: 3px; 14 | right: 3px; 15 | 16 | width: 30px; 17 | height: 30px; 18 | padding: 7px; 19 | } 20 | -------------------------------------------------------------------------------- /app/server/src/decorators/user-email.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const UserEmail = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.user; 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /app/server/.dockerignore: -------------------------------------------------------------------------------- 1 | **/mongo-data-4.4/ 2 | 3 | **/.github 4 | **/.git 5 | **/.gitignore 6 | 7 | **/node_modules/ 8 | 9 | **/dist/ 10 | 11 | **/.eslintignore 12 | **/.eslintrc 13 | **/.prettierignore 14 | **/.prettierrc 15 | 16 | **/Dockerfile 17 | **/.dockerignore 18 | 19 | **/README.md 20 | **/Readme.md 21 | 22 | **/test/ -------------------------------------------------------------------------------- /app/server/src/configs/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import {ConfigService} from '@nestjs/config'; 2 | import {JwtModuleOptions} from '@nestjs/jwt'; 3 | 4 | export const getJWTConfig = async ( 5 | configService: ConfigService, 6 | ): Promise => { 7 | return { 8 | secret: configService.get('JWT_SECRET'), 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /app/client/components/Input/Input.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, InputHTMLAttributes } from 'react'; 2 | import { FieldError } from 'react-hook-form'; 3 | 4 | export interface IInputProps 5 | extends DetailedHTMLProps< 6 | InputHTMLAttributes, 7 | HTMLInputElement 8 | > { 9 | error?: FieldError; 10 | } 11 | -------------------------------------------------------------------------------- /app/client/components/Advantages/Advantages.module.css: -------------------------------------------------------------------------------- 1 | .advantage { 2 | display: grid; 3 | grid-template-columns: 50px 1fr; 4 | gap: 10px 40px; 5 | 6 | margin-bottom: 30px; 7 | } 8 | 9 | .title { 10 | align-self: center; 11 | 12 | font-weight: bold; 13 | } 14 | 15 | .vline { 16 | border-left: 1px solid var(--gray-light); 17 | } 18 | -------------------------------------------------------------------------------- /app/client/components/Sort/Sort.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | export enum ESort { 4 | Rating, 5 | Price, 6 | } 7 | 8 | export interface ISortProps 9 | extends DetailedHTMLProps, HTMLDivElement> { 10 | sort: ESort; 11 | setSort: (sort: ESort) => void; 12 | } 13 | -------------------------------------------------------------------------------- /app/client/components/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import styles from './Divider.module.css'; 3 | import { IDividerProps } from './Divider.props'; 4 | 5 | export const Divider = ({ 6 | className, 7 | ...props 8 | }: IDividerProps): JSX.Element => { 9 | return
; 10 | }; 11 | -------------------------------------------------------------------------------- /app/client/components/TextArea/TextArea.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, TextareaHTMLAttributes } from 'react'; 2 | import { FieldError } from 'react-hook-form'; 3 | 4 | export interface ITextAreaProps 5 | extends DetailedHTMLProps< 6 | TextareaHTMLAttributes, 7 | HTMLTextAreaElement 8 | > { 9 | error?: FieldError; 10 | } 11 | -------------------------------------------------------------------------------- /app/client/components/Tag/Tag.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'; 2 | 3 | export interface ITagProps 4 | extends DetailedHTMLProps, HTMLDivElement> { 5 | size?: 's' | 'm'; 6 | children: ReactNode; 7 | color?: 'ghost' | 'red' | 'grey' | 'green' | 'primary'; 8 | href?: string; 9 | } 10 | -------------------------------------------------------------------------------- /app/server/src/auth/user.model.ts: -------------------------------------------------------------------------------- 1 | import { prop } from '@typegoose/typegoose'; 2 | import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'; 3 | 4 | export interface UserModel extends Base {} 5 | 6 | export class UserModel extends TimeStamps { 7 | @prop({ unique: true }) 8 | email: string; 9 | 10 | @prop() 11 | passwordHash: string; 12 | } 13 | -------------------------------------------------------------------------------- /app/client/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['courses-top.ru'], 4 | }, 5 | reactStrictMode: true, 6 | webpack(config) { 7 | config.module.rules.push({ 8 | test: /\.svg$/, 9 | issuer: { 10 | test: /\.(js|ts)x?$/, 11 | }, 12 | use: ['@svgr/webpack'], 13 | }); 14 | 15 | return config; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /app/client/page-components/TopPageComponent/TopPageComponent.props.ts: -------------------------------------------------------------------------------- 1 | import { ETopLevelCategory, ITopPageModel } from 'interfaces/page.interface'; 2 | import { IProductModel } from 'interfaces/product.interface'; 3 | 4 | export interface ITopPageComponentProps extends Record { 5 | firstCategory: ETopLevelCategory; 6 | page: ITopPageModel; 7 | products: IProductModel[]; 8 | } 9 | -------------------------------------------------------------------------------- /app/client/components/Up/Up.module.css: -------------------------------------------------------------------------------- 1 | .up { 2 | position: fixed; 3 | right: 30px; 4 | bottom: 30px; 5 | 6 | width: 40px; 7 | height: 40px; 8 | 9 | cursor: pointer; 10 | 11 | border: none; 12 | border-radius: 10px; 13 | background: var(--primary); 14 | box-shadow: 0 4px 4px rgba(0, 0, 0.05); 15 | } 16 | 17 | .up:hover { 18 | background-color: var(--primary-hover); 19 | } 20 | -------------------------------------------------------------------------------- /app/client/components/Rating/Rating.props.ts: -------------------------------------------------------------------------------- 1 | import { DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | import { FieldError } from 'react-hook-form'; 3 | 4 | export interface IRatingProps 5 | extends DetailedHTMLProps, HTMLDivElement> { 6 | isEditable?: boolean; 7 | rating: number; 8 | setRating?: (rating: number) => void; 9 | error?: FieldError; 10 | } 11 | -------------------------------------------------------------------------------- /app/server/src/telegram/telegram.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from '@nestjs/common'; 2 | 3 | export interface ITelegramOptions { 4 | chatId: string; 5 | token: string; 6 | } 7 | 8 | export interface ITelegramModuleAsyncOptions 9 | extends Pick { 10 | useFactory: (...args: any[]) => Promise | ITelegramOptions; 11 | inject?: any[]; 12 | } 13 | -------------------------------------------------------------------------------- /app/client/components/Htag/Htag.module.css: -------------------------------------------------------------------------------- 1 | .h1 { 2 | margin: 0; 3 | 4 | font-size: 26px; 5 | font-weight: 500; 6 | line-height: 35px; 7 | } 8 | 9 | .h2 { 10 | margin-top: 50px; 11 | margin-bottom: 25px; 12 | 13 | font-size: 22px; 14 | font-weight: 500; 15 | line-height: 30px; 16 | } 17 | 18 | .h3 { 19 | margin: 0; 20 | 21 | font-size: 20px; 22 | font-weight: 600; 23 | line-height: 27px; 24 | } 25 | -------------------------------------------------------------------------------- /app/client/helpers/api.ts: -------------------------------------------------------------------------------- 1 | export const API = { 2 | topPage: { 3 | find: process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/find', 4 | byAlias: process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/byAlias/', 5 | }, 6 | product: { 7 | find: process.env.NEXT_PUBLIC_DOMAIN + '/api/product/find', 8 | }, 9 | review: { 10 | createDemo: process.env.NEXT_PUBLIC_DOMAIN + '/api/review/create-demo', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /app/server/src/hh/hh.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { HhService } from 'hh/hh.service'; 4 | import { HhController } from './hh.controller'; 5 | 6 | @Module({ 7 | providers: [HhService], 8 | controllers: [HhController], 9 | imports: [ConfigModule, HttpModule], 10 | exports: [HhService], 11 | }) 12 | export class HhModule {} 13 | -------------------------------------------------------------------------------- /app/server/src/review/dto/create-review.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsString, Max, Min } from 'class-validator'; 2 | 3 | export class CreateReviewDto { 4 | @IsString() 5 | name: string; 6 | 7 | @IsString() 8 | title: string; 9 | 10 | @IsString() 11 | description: string; 12 | 13 | @IsNumber() 14 | @Max(5) 15 | @Min(1, { message: 'Рейтинг не может быть менее 1' }) 16 | rating: number; 17 | 18 | @IsString() 19 | productId: string; 20 | } 21 | -------------------------------------------------------------------------------- /app/client/layout/Footer/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: grid; 3 | 4 | padding: 25px 30px; 5 | 6 | color: var(--white); 7 | 8 | background: var(--primary); 9 | 10 | font-size: 16px; 11 | line-height: 20px; 12 | grid-template-columns: 1fr auto auto; 13 | gap: 10px 40px; 14 | } 15 | 16 | .footer a:hover { 17 | color: var(--gray-light); 18 | } 19 | 20 | @media (max-width: 765px) { 21 | .footer { 22 | grid-template-columns: 1fr; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/client/components/ButtonIcon/up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/Button/Button.props.ts: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react'; 2 | 3 | export interface IButtonProps 4 | extends Omit< 5 | DetailedHTMLProps< 6 | ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | >, 9 | 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag' | 'ref' 10 | > { 11 | children: ReactNode; 12 | appearance: 'primary' | 'ghost'; 13 | arrow?: 'right' | 'down' | 'none'; 14 | } 15 | -------------------------------------------------------------------------------- /app/client/components/Rating/Rating.module.css: -------------------------------------------------------------------------------- 1 | .filled svg { 2 | fill: var(--primary); 3 | } 4 | 5 | .star { 6 | display: inline-block; 7 | } 8 | 9 | .star svg { 10 | margin-right: 5px; 11 | } 12 | 13 | .editable { 14 | cursor: pointer; 15 | } 16 | 17 | .ratingWrapper { 18 | position: relative; 19 | } 20 | 21 | .errorMessage { 22 | position: absolute; 23 | bottom: -20px; 24 | left: 0; 25 | 26 | color: var(--red); 27 | } 28 | 29 | .error svg { 30 | stroke: var(--red); 31 | } 32 | -------------------------------------------------------------------------------- /app/server/src/configs/telegram.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { ITelegramOptions } from 'telegram/telegram.interface'; 3 | 4 | export const getTelegramConfig = ( 5 | configService: ConfigService, 6 | ): ITelegramOptions => { 7 | const token = configService.get('TELEGRAM_TOKEN'); 8 | if (!token) { 9 | throw new Error('TELEGRAM_TOKEN не задан!'); 10 | } 11 | return { 12 | token, 13 | chatId: configService.get('CHAT_ID') ?? '', 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /app/client/components/Htag/Htag.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Htag.module.css'; 2 | import { IHtagProps } from './Htag.props'; 3 | 4 | export const Htag = ({ tag, children }: IHtagProps): JSX.Element => { 5 | switch (tag) { 6 | case 'h1': 7 | return

{children}

; 8 | case 'h2': 9 | return

{children}

; 10 | case 'h3': 11 | return

{children}

; 12 | default: 13 | return <>; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /app/server/src/review/review.model.ts: -------------------------------------------------------------------------------- 1 | import {prop} from '@typegoose/typegoose'; 2 | import {Base, TimeStamps} from '@typegoose/typegoose/lib/defaultClasses'; 3 | import {Types} from 'mongoose'; 4 | 5 | export interface ReviewModel extends Base {} 6 | export class ReviewModel extends TimeStamps { 7 | @prop() 8 | name: string; 9 | 10 | @prop() 11 | title: string; 12 | 13 | @prop() 14 | description: string; 15 | 16 | @prop() 17 | rating: number; 18 | 19 | @prop() 20 | productId: Types.ObjectId; 21 | } 22 | -------------------------------------------------------------------------------- /app/client/interfaces/menu.interface.ts: -------------------------------------------------------------------------------- 1 | import { ETopLevelCategory } from 'interfaces/page.interface'; 2 | 3 | export interface IPageItem { 4 | alias: string; 5 | title: string; 6 | _id: string; 7 | category: string; 8 | } 9 | 10 | export interface IMenuItem { 11 | _id: { 12 | secondCategory: string; 13 | }; 14 | isOpened?: boolean; 15 | pages: IPageItem[]; 16 | } 17 | 18 | export interface IFirstLevelMenuItem { 19 | route: string; 20 | name: string; 21 | icon: JSX.Element; 22 | id: ETopLevelCategory; 23 | } 24 | -------------------------------------------------------------------------------- /app/client/components/P/P.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import styles from './P.module.css'; 3 | import { IPProps } from './P.props'; 4 | 5 | export const P = ({ 6 | size = 'm', 7 | children, 8 | className, 9 | ...props 10 | }: IPProps): JSX.Element => { 11 | return ( 12 |

20 | {children} 21 |

22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app/server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /uploads/ 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /app/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | #.env.local 29 | #.env.development.local 30 | #.env.test.local 31 | #.env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /app/server/src/files/files.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ServeStaticModule } from '@nestjs/serve-static'; 3 | import { path } from 'app-root-path'; 4 | import { FilesController } from './files.controller'; 5 | import { FilesService } from './files.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | ServeStaticModule.forRoot({ 10 | rootPath: `${path}/uploads`, 11 | serveRoot: '/static/', 12 | }), 13 | ], 14 | controllers: [FilesController], 15 | providers: [FilesService], 16 | }) 17 | export class FilesModule {} 18 | -------------------------------------------------------------------------------- /app/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "rules": { 6 | "semi": "off", 7 | "@typescript-eslint/semi": ["warn"], 8 | "@typescript-eslint/no-empty-interface": [ 9 | "error", 10 | { 11 | "allowSingleExtends": true 12 | } 13 | ] 14 | }, 15 | "extends": [ 16 | "eslint:recommended", 17 | "plugin:@typescript-eslint/eslint-recommended", 18 | "plugin:@typescript-eslint/recommended", 19 | "plugin:react-hooks/recommended" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /app/client/components/ButtonIcon/ButtonIcon.props.ts: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, DetailedHTMLProps } from 'react'; 2 | import close from './close.svg'; 3 | import menu from './menu.svg'; 4 | import up from './up.svg'; 5 | 6 | export const icons = { 7 | up, 8 | close, 9 | menu, 10 | }; 11 | 12 | export type TIconName = keyof typeof icons; 13 | 14 | export interface IButtonIconProps 15 | extends DetailedHTMLProps< 16 | ButtonHTMLAttributes, 17 | HTMLButtonElement 18 | > { 19 | icon: TIconName; 20 | appearance: 'primary' | 'white'; 21 | } 22 | -------------------------------------------------------------------------------- /app/client/helpers/icons/services.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/layout/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import { Search } from 'components'; 3 | import { Menu } from 'layout/Menu/Menu'; 4 | import Logo from '../logo.svg'; 5 | import styles from './Sidebar.module.css'; 6 | import { ISidebarProps } from './Sidebar.props'; 7 | 8 | export const Sidebar = ({ 9 | className, 10 | ...props 11 | }: ISidebarProps): JSX.Element => { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /app/client/hooks/useScrollY.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useScrollY = (): number => { 4 | const isBrowser = typeof window !== 'undefined'; 5 | const [scrollY, setScrollY] = useState(0); 6 | 7 | const handleScroll = () => { 8 | const currentScrollY = isBrowser ? window.scrollY : 0; 9 | setScrollY(currentScrollY); 10 | }; 11 | 12 | useEffect(() => { 13 | window.addEventListener('scroll', handleScroll, { passive: true }); 14 | return () => window.removeEventListener('scroll', handleScroll); 15 | }, []); 16 | 17 | return scrollY; 18 | }; 19 | -------------------------------------------------------------------------------- /app/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "./src", 5 | "module": "commonjs", 6 | "target": "es2017", 7 | "skipLibCheck": true, 8 | "strictPropertyInitialization": false, 9 | "declaration": true, 10 | "removeComments": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "outDir": "./dist", 16 | "incremental": true 17 | }, 18 | "exclude": ["node_modules"], 19 | "include": ["./src/**/*.ts", "./test/**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /app/client/layout/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: none; 3 | } 4 | 5 | .mobileMenu { 6 | position: fixed; 7 | z-index: 10; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | 13 | overflow-y: scroll; 14 | 15 | padding: 20px 10px; 16 | 17 | background: #F4F6F8; 18 | } 19 | 20 | .menuClose { 21 | position: fixed; 22 | z-index: 11; 23 | top: 15px; 24 | right: 15px; 25 | } 26 | 27 | @media (max-width: 765px) { 28 | .header { 29 | display: grid; 30 | grid-template-columns: 1fr 40px; 31 | gap: 10px; 32 | 33 | margin: 15px 15px 0 15px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/client-push-images-to-github.yml.disabled: -------------------------------------------------------------------------------- 1 | name: Client Publish Docker 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Publish to registry 13 | uses: elgohr/Publish-Docker-Github-Action@master 14 | with: 15 | registry: docker.pkg.github.com 16 | name: docker.pkg.github.com/AlariCode/courses-top/courses-top 17 | username: ${{ secrets.DOCKER_USERNAME }} 18 | password: ${{ secrets.DOCKER_PASSWORD }} 19 | tags: 'develop' 20 | -------------------------------------------------------------------------------- /.github/workflows/server-push-images-to-github.yml.disabled: -------------------------------------------------------------------------------- 1 | name: Server Publish Docker 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Publish to registry 13 | uses: elgohr/Publish-Docker-Github-Action@master 14 | with: 15 | registry: docker.pkg.github.com 16 | name: docker.pkg.github.com/AlariCode/top-api-demo/top-api-demo 17 | username: ${{ secrets.DOCKER_USERNAME }} 18 | password: ${{ secrets.DOCKER_PASSWORD }} 19 | tags: 'develop' 20 | -------------------------------------------------------------------------------- /app/client/components/Advantages/Advantages.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Advantages.module.css'; 2 | import { IAdvantagesProps } from './Advantages.props'; 3 | import CheckIcon from './check.svg'; 4 | 5 | export const Advantages = ({ advantages }: IAdvantagesProps): JSX.Element => { 6 | return ( 7 | <> 8 | {advantages.map((a) => ( 9 |
10 | 11 |
{a.title}
12 |
13 |
{a.description}
14 |
15 | ))} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /app/client/helpers/icons/courses.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/server/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | top.api: 4 | image: docker.pkg.github.com/AlariCode/top-api-demo/top-api-demo:develop 5 | container_name: top-api 6 | restart: always 7 | ports: 8 | - 3000:3000 9 | volumes: 10 | - ./.env:/app/.env 11 | mongo: 12 | image: mongo:4.4.6 13 | container_name: mongo 14 | restart: always 15 | environment: 16 | - MONGO_INITDB_ROOT_USERNAME=admin 17 | - MONGO_INITDB_ROOT_PASSWORD=admin 18 | ports: 19 | - 27017:27017 20 | volumes: 21 | - ./mongo-data-4.4:/data/db 22 | command: --wiredTigerCacheSizeGB 1.5 23 | -------------------------------------------------------------------------------- /app/client/components/Button/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/Advantages/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "./", 5 | "target": "es2017", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "strictPropertyInitialization": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /app/server/src/pipes/id-validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentMetadata, 3 | BadRequestException, 4 | Injectable, 5 | PipeTransform, 6 | } from '@nestjs/common'; 7 | import { Types } from 'mongoose'; 8 | import { ID_VALIDATION_ERROR } from 'pipes/id-validation.constants'; 9 | 10 | @Injectable() 11 | export class IdValidationPipe implements PipeTransform { 12 | transform(value: string, metadata: ArgumentMetadata) { 13 | if (metadata.type != 'param') { 14 | return value; 15 | } 16 | if (!Types.ObjectId.isValid(value)) { 17 | throw new BadRequestException(ID_VALIDATION_ERROR); 18 | } 19 | return value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/client/components/ButtonIcon/ButtonIcon.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import styles from './ButtonIcon.module.css'; 3 | import { IButtonIconProps, icons } from './ButtonIcon.props'; 4 | 5 | export const ButtonIcon = ({ 6 | appearance, 7 | icon, 8 | className, 9 | ...props 10 | }: IButtonIconProps): JSX.Element => { 11 | const IconComp = icons[icon]; 12 | 13 | return ( 14 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /app/client/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import { ForwardedRef, forwardRef } from 'react'; 3 | import styles from './Card.module.css'; 4 | import { ICardProps } from './Card.props'; 5 | 6 | export const Card = forwardRef( 7 | ( 8 | { color = 'white', children, className, ...props }: ICardProps, 9 | ref: ForwardedRef 10 | ): JSX.Element => { 11 | return ( 12 |
19 | {children} 20 |
21 | ); 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /app/server/src/product/product.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypegooseModule } from 'nestjs-typegoose'; 3 | import { ProductModel } from 'product/product.model'; 4 | import { ProductController } from './product.controller'; 5 | import { ProductService } from './product.service'; 6 | 7 | @Module({ 8 | controllers: [ProductController], 9 | imports: [ 10 | TypegooseModule.forFeature([ 11 | { 12 | typegooseClass: ProductModel, 13 | schemaOptions: { 14 | collection: 'Product', 15 | }, 16 | }, 17 | ]), 18 | ], 19 | providers: [ProductService], 20 | }) 21 | export class ProductModule {} 22 | -------------------------------------------------------------------------------- /app/client/layout/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import { format } from 'date-fns'; 3 | import styles from './Footer.module.css'; 4 | import { IFooterProps } from './Footer.props'; 5 | 6 | export const Footer = ({ className, ...props }: IFooterProps): JSX.Element => { 7 | return ( 8 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /app/client/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Advantages/Advantages'; 2 | export * from './Button/Button'; 3 | export * from './ButtonIcon/ButtonIcon'; 4 | export * from './Card/Card'; 5 | export * from './Divider/Divider'; 6 | export * from './HhData/HhData'; 7 | export * from './Htag/Htag'; 8 | export * from './Input/Input'; 9 | export * from './P/P'; 10 | export * from './Product/Product'; 11 | export * from './Rating/Rating'; 12 | export * from './Review/Review'; 13 | export * from './ReviewForm/ReviewForm'; 14 | export * from './Search/Search'; 15 | export * from './Sort/Sort'; 16 | export * from './Tag/Tag'; 17 | export * from './TextArea/TextArea'; 18 | export * from './Up/Up'; 19 | -------------------------------------------------------------------------------- /app/client/components/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | padding: 7px 15px; 3 | 4 | color: var(--black); 5 | border: none; 6 | border-radius: 5px; 7 | outline-color: var(--primary); 8 | background: var(--white); 9 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05); 10 | 11 | font-family: var(--font-family); 12 | 13 | font-size: 16px; 14 | line-height: 22px; 15 | } 16 | 17 | .input::placeholder { 18 | color: var(--gray); 19 | } 20 | 21 | .error { 22 | border: 1px solid var(--red); 23 | } 24 | 25 | .inputWrapper { 26 | position: relative; 27 | } 28 | 29 | .errorMessage { 30 | position: absolute; 31 | bottom: -20px; 32 | left: 0; 33 | 34 | color: var(--red); 35 | } 36 | -------------------------------------------------------------------------------- /app/client/components/ButtonIcon/ButtonIcon.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | width: 40px; 3 | height: 40px; 4 | 5 | cursor: pointer; 6 | 7 | border: none; 8 | border-radius: 10px; 9 | box-shadow: 0 4px 4px rgba(0, 0, 0.05); 10 | } 11 | 12 | .primary { 13 | background-color: var(--primary); 14 | } 15 | 16 | .primary svg * { 17 | fill: var(--white); 18 | } 19 | 20 | .primary:hover { 21 | background-color: var(--primary-hover); 22 | } 23 | 24 | .white { 25 | background-color: var(--white); 26 | } 27 | 28 | .white svg * { 29 | fill: var(--primary); 30 | } 31 | 32 | .white:hover { 33 | background-color: var(--primary); 34 | } 35 | 36 | .white:hover svg * { 37 | fill: var(--white); 38 | } 39 | -------------------------------------------------------------------------------- /app/client/components/Search/glass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/server/src/telegram/telegram.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { Telegraf } from 'telegraf'; 3 | import { TELEGRAM_MODULE_OPTIONS } from 'telegram/telegram.constants'; 4 | import { ITelegramOptions } from 'telegram/telegram.interface'; 5 | 6 | @Injectable() 7 | export class TelegramService { 8 | bot: Telegraf; 9 | options: ITelegramOptions; 10 | 11 | constructor(@Inject(TELEGRAM_MODULE_OPTIONS) options: ITelegramOptions) { 12 | this.bot = new Telegraf(options.token); 13 | this.options = options; 14 | } 15 | 16 | async sendMessage(message: string, chatId: string = this.options.chatId) { 17 | await this.bot.telegram.sendMessage(chatId, message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/client/components/TextArea/TextArea.module.css: -------------------------------------------------------------------------------- 1 | .textarea { 2 | width: 100%; 3 | padding: 7px 15px; 4 | 5 | color: var(--black); 6 | border: none; 7 | border-radius: 5px; 8 | outline-color: var(--primary); 9 | background: var(--white); 10 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05); 11 | 12 | font-family: var(--font-family); 13 | 14 | font-size: 16px; 15 | line-height: 22px; 16 | } 17 | 18 | .textarea::placeholder { 19 | color: var(--gray); 20 | } 21 | 22 | .error { 23 | border: 1px solid var(--red); 24 | } 25 | 26 | .textareaWrapper { 27 | position: relative; 28 | } 29 | 30 | .errorMessage { 31 | position: absolute; 32 | bottom: -15px; 33 | left: 0; 34 | 35 | color: var(--red); 36 | } 37 | -------------------------------------------------------------------------------- /app/server/src/review/review.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {TypegooseModule} from 'nestjs-typegoose'; 3 | import {ReviewModel} from 'review/review.model'; 4 | import {TelegramModule} from 'telegram/telegram.module'; 5 | import {ReviewController} from './review.controller'; 6 | import {ReviewService} from './review.service'; 7 | 8 | @Module({ 9 | controllers: [ReviewController], 10 | imports: [ 11 | TypegooseModule.forFeature([ 12 | { 13 | typegooseClass: ReviewModel, 14 | schemaOptions: { 15 | collection: 'Review', 16 | }, 17 | }, 18 | ]), 19 | TelegramModule, 20 | ], 21 | providers: [ReviewService], 22 | }) 23 | export class ReviewModule {} 24 | -------------------------------------------------------------------------------- /app/server/src/top-page/top-page.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HhModule } from 'hh/hh.module'; 3 | import { TypegooseModule } from 'nestjs-typegoose'; 4 | import { TopPageModel } from 'top-page/top-page.model'; 5 | import { TopPageController } from './top-page.controller'; 6 | import { TopPageService } from './top-page.service'; 7 | 8 | @Module({ 9 | controllers: [TopPageController], 10 | imports: [ 11 | TypegooseModule.forFeature([ 12 | { 13 | typegooseClass: TopPageModel, 14 | schemaOptions: { 15 | collection: 'TopPage', 16 | }, 17 | }, 18 | ]), 19 | HhModule, 20 | ], 21 | providers: [TopPageService], 22 | }) 23 | export class TopPageModule {} 24 | -------------------------------------------------------------------------------- /app/server/src/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {ConfigService} from '@nestjs/config'; 3 | import {PassportStrategy} from '@nestjs/passport'; 4 | import {UserModel} from 'auth/user.model'; 5 | import {ExtractJwt, Strategy} from 'passport-jwt'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor(private readonly configService: ConfigService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: true, 13 | secretOrKey: configService.get('JWT_SECRET'), 14 | }); 15 | } 16 | 17 | async validate({ email }: Pick) { 18 | return email; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/client/helpers/icons/products.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/Rating/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/Sort/Sort.module.css: -------------------------------------------------------------------------------- 1 | .sortIcon { 2 | display: none; 3 | } 4 | 5 | .active { 6 | color: var(--primary); 7 | 8 | font-weight: bold; 9 | } 10 | 11 | .active .sortIcon { 12 | display: block; 13 | 14 | margin-right: 8px; 15 | } 16 | 17 | .sort { 18 | display: inline-block; 19 | display: grid; 20 | grid-template-columns: auto auto; 21 | gap: 40px; 22 | } 23 | 24 | .sortName { 25 | display: none; 26 | } 27 | 28 | .sort button { 29 | display: grid; 30 | gap: 8px; 31 | grid-template-columns: 20px 1fr; 32 | 33 | align-items: center; 34 | 35 | cursor: pointer; 36 | 37 | border: none; 38 | background: none; 39 | 40 | font-size: 16px; 41 | 42 | line-height: 22px; 43 | } 44 | 45 | .sort button:not(.active) { 46 | grid-template-columns: 1fr; 47 | } 48 | -------------------------------------------------------------------------------- /app/server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | '@typescript-eslint/no-empty-interface': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /app/client/pages/search.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API } from 'helpers/api'; 3 | import { GetStaticProps } from 'next'; 4 | import { IMenuItem } from '../interfaces/menu.interface'; 5 | import { withLayout } from '../layout/Layout'; 6 | 7 | function Search(): JSX.Element { 8 | return <>Search; 9 | } 10 | 11 | export default withLayout(Search); 12 | 13 | export const getStaticProps: GetStaticProps = async () => { 14 | const firstCategory = 0; 15 | const { data: menu } = await axios.post(API.topPage.find, { 16 | firstCategory, 17 | }); 18 | return { 19 | props: { 20 | menu, 21 | firstCategory, 22 | }, 23 | }; 24 | }; 25 | 26 | interface IHomeProps extends Record { 27 | menu: IMenuItem[]; 28 | firstCategory: number; 29 | } 30 | -------------------------------------------------------------------------------- /app/client/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | 6 | color: var(--black); 7 | background: #F5F6F8; 8 | 9 | font-family: 'Noto Sans KR', sans-serif; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | 15 | color: inherit; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | .visualyHidden { 23 | position: absolute; 24 | 25 | overflow: hidden; 26 | 27 | width: 0; 28 | height: 0; 29 | } 30 | 31 | :root { 32 | --black: #3B434E; 33 | --gray-light: #EBEBEB; 34 | --gray: #A4A4A4; 35 | --gray-dark: #6C7077; 36 | --white: white; 37 | --primary: #563CB8; 38 | --primary-hover: #6344DF; 39 | --red-light: #FEA291; 40 | --red: #FC836D; 41 | --green: #007B48; 42 | --green-light: #C8F8E4; 43 | --font-family: 'Noto Sans KR', sans-serif; 44 | } 45 | -------------------------------------------------------------------------------- /app/client/components/HhData/rate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/components/Tag/Tag.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import styles from './Tag.module.css'; 3 | import { ITagProps } from './Tag.props'; 4 | 5 | export const Tag = ({ 6 | size = 's', 7 | children, 8 | color = 'ghost', 9 | href, 10 | className, 11 | ...props 12 | }: ITagProps): JSX.Element => { 13 | return ( 14 |
26 | {href ? {children} : <>{children}} 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /app/client/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import { ForwardedRef, forwardRef } from 'react'; 3 | import styles from './Input.module.css'; 4 | import { IInputProps } from './Input.props'; 5 | 6 | export const Input = forwardRef( 7 | ( 8 | { className, error, ...props }: IInputProps, 9 | ref: ForwardedRef 10 | ): JSX.Element => { 11 | return ( 12 |
13 | 20 | {error && ( 21 | 22 | {error.message} 23 | 24 | )} 25 |
26 | ); 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /app/server/src/configs/mongo.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { TypegooseModuleOptions } from 'nestjs-typegoose'; 3 | 4 | export const getMongoConfig = async ( 5 | configService: ConfigService, 6 | ): Promise => { 7 | return { 8 | uri: getMongoString(configService), 9 | ...getMongoOptions(), 10 | }; 11 | }; 12 | 13 | const getMongoString = (configService: ConfigService) => 14 | 'mongodb://' + 15 | configService.get('MONGO_LOGIN') + 16 | ':' + 17 | configService.get('MONGO_PASSWORD') + 18 | '@' + 19 | configService.get('MONGO_HOST') + 20 | ':' + 21 | configService.get('MONGO_PORT') + 22 | '/' + 23 | configService.get('MONGO_DATABASE_NAME'); 24 | 25 | const getMongoOptions = () => ({ 26 | useNewUrlParser: true, 27 | useCreateIndex: true, 28 | useUnifiedTopology: true, 29 | }); 30 | -------------------------------------------------------------------------------- /app/client/interfaces/product.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IProductCharacteristic { 2 | value: string; 3 | name: string; 4 | } 5 | 6 | export interface IReviewModel { 7 | _id: string; 8 | name: string; 9 | title: string; 10 | description: string; 11 | rating: number; 12 | createdAt: Date; 13 | } 14 | 15 | export interface IProductModel { 16 | _id: string; 17 | categories: string[]; 18 | tags: string[]; 19 | title: string; 20 | link: string; 21 | price: number; 22 | credit: number; 23 | oldPrice: number; 24 | description: string; 25 | characteristics: IProductCharacteristic[]; 26 | createdAt: Date; 27 | updatedAt: Date; 28 | __v: number; 29 | image: string; 30 | initialRating: number; 31 | reviews: IReviewModel[]; 32 | reviewCount: number; 33 | reviewAvg?: number; 34 | advantages?: string; 35 | disadvantages?: string; 36 | } 37 | -------------------------------------------------------------------------------- /app/client/components/TextArea/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import { ForwardedRef, forwardRef } from 'react'; 3 | import styles from './TextArea.module.css'; 4 | import { ITextAreaProps } from './TextArea.props'; 5 | 6 | export const TextArea = forwardRef( 7 | ( 8 | { error, className, ...props }: ITextAreaProps, 9 | ref: ForwardedRef 10 | ): JSX.Element => { 11 | return ( 12 |
13 |