├── .env.example ├── .eslintrc.json ├── .editorconfig ├── prettier.config.js ├── src ├── components │ ├── pages │ │ ├── Home │ │ │ ├── Home.module.scss │ │ │ ├── Home.tsx │ │ │ ├── QuestionsSection │ │ │ │ ├── QuestionsSection.module.scss │ │ │ │ ├── QuestionsSection.data.ts │ │ │ │ └── QuestionsSection.tsx │ │ │ ├── FormSection │ │ │ │ ├── FormSection.module.scss │ │ │ │ └── FormSection.tsx │ │ │ ├── PrimarySection │ │ │ │ ├── PrimarySection.module.scss │ │ │ │ └── PrimarySection.tsx │ │ │ └── CategoriesSection │ │ │ │ └── CategoriesSection.module.scss │ │ ├── Contacts │ │ │ ├── Contacts.module.scss │ │ │ ├── Contacts.tsx │ │ │ └── ContactsSection │ │ │ │ ├── ContactsSection.module.scss │ │ │ │ └── ContactsSection.tsx │ │ └── Profile │ │ │ ├── Teacher │ │ │ ├── ScheduleModal │ │ │ │ ├── ScheduleModal.types.ts │ │ │ │ ├── ScheduleModalItem │ │ │ │ │ ├── ScheduleDeleteModal │ │ │ │ │ │ ├── ScheduleDeleteModal.module.scss │ │ │ │ │ │ └── ScheduleDeleteModal.tsx │ │ │ │ │ ├── ScheduleModalItem.module.scss │ │ │ │ │ └── ScheduleModalItem.tsx │ │ │ │ ├── ScheduleModal.data.ts │ │ │ │ └── ScheduleModal.module.scss │ │ │ ├── ScheduleCard │ │ │ │ ├── ScheduleCard.module.scss │ │ │ │ └── ScheduleCard.tsx │ │ │ ├── TeacherScheduleItem │ │ │ │ ├── TeacherScheduleItem.module.scss │ │ │ │ └── TeacherScheduleItem.tsx │ │ │ └── Teacher.tsx │ │ │ ├── ErrorMessage │ │ │ ├── ErrorMessage.module.scss │ │ │ └── ErrorMessage.tsx │ │ │ ├── Admin │ │ │ ├── GroupsSection │ │ │ │ ├── GroupDeleteModal │ │ │ │ │ ├── GroupDeleteModal.module.scss │ │ │ │ │ └── GroupDeleteModal.tsx │ │ │ │ ├── GroupSearch │ │ │ │ │ ├── GroupSearch.module.scss │ │ │ │ │ └── GroupSearch.tsx │ │ │ │ ├── GroupsSection.module.scss │ │ │ │ ├── GroupItem │ │ │ │ │ ├── GroupItem.module.scss │ │ │ │ │ └── GroupItem.tsx │ │ │ │ ├── GroupCreateModal │ │ │ │ │ ├── GroupCreateModal.module.scss │ │ │ │ │ └── GroupCreateModal.tsx │ │ │ │ ├── GroupChangeModal │ │ │ │ │ ├── GroupChangeModal.module.scss │ │ │ │ │ └── GroupChangeModal.tsx │ │ │ │ └── GroupsSection.tsx │ │ │ ├── CategoriesSection │ │ │ │ ├── CategoryItem │ │ │ │ │ ├── CategoryItem.module.scss │ │ │ │ │ └── CategoryItem.tsx │ │ │ │ ├── CategoryDeleteModal │ │ │ │ │ ├── CategoryDeleteModal.module.scss │ │ │ │ │ └── CategoryDeleteModal.tsx │ │ │ │ ├── CategoriesSearch │ │ │ │ │ ├── CategoriesSearch.module.scss │ │ │ │ │ └── CategoriesSearch.tsx │ │ │ │ ├── CategoryChangeModal │ │ │ │ │ ├── CategoryChangeModal.module.scss │ │ │ │ │ └── CategoryChangeModal.tsx │ │ │ │ ├── CategoryCreateModal │ │ │ │ │ ├── CategoryCreateModal.module.scss │ │ │ │ │ └── CategoryCreateModal.tsx │ │ │ │ ├── CategoriesSection.module.scss │ │ │ │ └── CategoriesSection.tsx │ │ │ ├── StudentsSection │ │ │ │ ├── StudentDeleteModal │ │ │ │ │ ├── StudentDeleteModal.module.scss │ │ │ │ │ └── StudentDeleteModal.tsx │ │ │ │ ├── StudentSearch │ │ │ │ │ ├── StudentSearch.module.scss │ │ │ │ │ └── StudentSearch.tsx │ │ │ │ ├── StudentCreateModal │ │ │ │ │ ├── StudentCreateModal.module.scss │ │ │ │ │ ├── StudentCreateItem │ │ │ │ │ │ ├── StudentCreateItem.module.scss │ │ │ │ │ │ └── StudentCreateItem.tsx │ │ │ │ │ └── StudentCreateModal.tsx │ │ │ │ ├── StudentItem │ │ │ │ │ ├── StudentItem.module.scss │ │ │ │ │ └── StudentItem.tsx │ │ │ │ ├── StudentsSection.module.scss │ │ │ │ └── StudentsSection.tsx │ │ │ ├── InstructorsSection │ │ │ │ ├── InstructorDeleteModal │ │ │ │ │ ├── InstructorDeleteModal.module.scss │ │ │ │ │ └── InstructorDeleteModal.tsx │ │ │ │ ├── InstructorsSearch │ │ │ │ │ ├── InstructorsSearch.module.scss │ │ │ │ │ └── InstructorsSearch.tsx │ │ │ │ ├── InstructorCreateModal │ │ │ │ │ ├── InstructorCreateModal.module.scss │ │ │ │ │ ├── InstructorCreateItem │ │ │ │ │ │ ├── InstructorCreateItem.module.scss │ │ │ │ │ │ └── InstructorCreateItem.tsx │ │ │ │ │ └── InstructorCreateModal.tsx │ │ │ │ ├── InstructorItem │ │ │ │ │ ├── InstructorItem.module.scss │ │ │ │ │ └── InstructorItem.tsx │ │ │ │ ├── InstructorsSection.module.scss │ │ │ │ ├── InstructorChangeModal │ │ │ │ │ ├── InstructorChangeModal.module.scss │ │ │ │ │ └── InstructorChangeModal.tsx │ │ │ │ └── InstructorsSection.tsx │ │ │ ├── NavigationCard │ │ │ │ ├── NavigationCard.module.scss │ │ │ │ └── NavigationCard.tsx │ │ │ └── Admin.tsx │ │ │ ├── Profile.module.scss │ │ │ ├── Student │ │ │ ├── TeachersCard │ │ │ │ ├── TeachersCard.module.scss │ │ │ │ └── TeachersCard.tsx │ │ │ ├── StudentScheduleItem │ │ │ │ ├── StudentScheduleItem.module.scss │ │ │ │ └── StudentScheduleItem.tsx │ │ │ └── Student.tsx │ │ │ ├── UserInfoCard │ │ │ ├── UserInfoCard.module.scss │ │ │ └── UserInfoCard.tsx │ │ │ └── Profile.tsx │ ├── other │ │ ├── Layout │ │ │ ├── Layout.module.scss │ │ │ └── Layout.tsx │ │ ├── Header │ │ │ ├── HeaderMobile │ │ │ │ ├── HeaderMobile.module.scss │ │ │ │ ├── Dropdown │ │ │ │ │ ├── Dropdown.module.scss │ │ │ │ │ └── Dropdown.tsx │ │ │ │ └── HeaderMobile.tsx │ │ │ ├── Header.module.scss │ │ │ ├── HeaderDesktop │ │ │ │ ├── HeaderDesktop.module.scss │ │ │ │ └── HeaderDesktop.tsx │ │ │ └── Header.tsx │ │ ├── Footer │ │ │ ├── Footer.module.scss │ │ │ └── Footer.tsx │ │ ├── ModalWrapper │ │ │ ├── ModalWrapper.module.scss │ │ │ ├── LoginModal │ │ │ │ ├── LoginModal.module.scss │ │ │ │ └── LoginModal.tsx │ │ │ ├── RegisterModal │ │ │ │ └── RegisterModal.module.scss │ │ │ └── ModalWrapper.tsx │ │ └── Icons │ │ │ ├── Burger.tsx │ │ │ ├── ExitThin.tsx │ │ │ ├── Cross.tsx │ │ │ ├── Arrow.tsx │ │ │ ├── ListArrow.tsx │ │ │ ├── LongArrow.tsx │ │ │ ├── Mail.tsx │ │ │ ├── Geo.tsx │ │ │ ├── Add.tsx │ │ │ ├── Phone.tsx │ │ │ ├── Error.tsx │ │ │ ├── Location.tsx │ │ │ ├── Avatar.tsx │ │ │ └── GroupAvatar.tsx │ └── UI │ │ ├── Logo │ │ ├── Logo.module.scss │ │ └── Logo.tsx │ │ ├── Select │ │ ├── Select.module.scss │ │ └── Select.tsx │ │ ├── Heading │ │ ├── Heading.module.scss │ │ └── Heading.tsx │ │ ├── Input │ │ ├── InputSecondary │ │ │ ├── InputSecondary.module.scss │ │ │ ├── InputSecondary.tsx │ │ │ └── InputSecondaryPhone │ │ │ │ └── InputSecondaryPhone.tsx │ │ └── InputPrimary │ │ │ ├── InputPrimary.module.scss │ │ │ ├── InputPrimary.tsx │ │ │ └── InputPrimaryPhone │ │ │ └── InputPrimaryPhone.tsx │ │ ├── Button │ │ ├── Button.module.scss │ │ └── Button.tsx │ │ └── Radio │ │ ├── Radio.tsx │ │ └── Radio.module.scss ├── styles │ ├── variables.scss │ └── globals.scss ├── images │ ├── icons │ │ ├── chart.png │ │ ├── fleet.png │ │ ├── price.png │ │ ├── theory.png │ │ ├── discount.png │ │ ├── practice.png │ │ └── instructor.png │ └── oversize │ │ ├── car.png │ │ ├── category_car_b.jpg │ │ ├── category_car_c.jpg │ │ ├── category_car_ce.jpg │ │ └── category_car_d.jpg ├── store │ ├── api │ │ ├── categories │ │ │ ├── categories.types.ts │ │ │ └── categories.api.ts │ │ ├── auth │ │ │ ├── auth.types.ts │ │ │ └── auth.api.ts │ │ ├── users │ │ │ ├── users.types.ts │ │ │ └── users.api.ts │ │ ├── groups │ │ │ ├── groups.types.ts │ │ │ └── groups.api.ts │ │ ├── schedules │ │ │ ├── schedules.types.ts │ │ │ └── schedules.api.ts │ │ └── api.ts │ ├── auth │ │ └── auth.slice.ts │ └── index.ts ├── hooks │ ├── useAuth.tsx │ ├── useTypedDispatch.ts │ ├── useTypedSelector.ts │ └── useDebounce.tsx ├── providers │ ├── PrivateRouter.types.ts │ ├── AuthProvider.tsx │ └── CheckRole.tsx ├── pages │ ├── index.tsx │ ├── _document.tsx │ ├── contacts │ │ └── index.tsx │ ├── profile │ │ └── index.tsx │ └── _app.tsx └── utils │ └── getScheduleType.utils.ts ├── public └── favicon.png ├── postcss.config.js ├── next.config.js ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | 5 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "none" 3 | }; 4 | -------------------------------------------------------------------------------- /src/components/pages/Home/Home.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | @apply space-y-16; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/pages/Contacts/Contacts.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | @apply flex flex-col gap-10; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $adaptive-min-breakpoint: "1200px"; 2 | $adaptive-max-breakpoint: "1199px"; 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/images/icons/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/chart.png -------------------------------------------------------------------------------- /src/images/icons/fleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/fleet.png -------------------------------------------------------------------------------- /src/images/icons/price.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/price.png -------------------------------------------------------------------------------- /src/images/icons/theory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/theory.png -------------------------------------------------------------------------------- /src/images/oversize/car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/oversize/car.png -------------------------------------------------------------------------------- /src/store/api/categories/categories.types.ts: -------------------------------------------------------------------------------- 1 | export interface CategoryType { 2 | id: number; 3 | value: string; 4 | } -------------------------------------------------------------------------------- /src/images/icons/discount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/discount.png -------------------------------------------------------------------------------- /src/images/icons/practice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/practice.png -------------------------------------------------------------------------------- /src/images/icons/instructor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/icons/instructor.png -------------------------------------------------------------------------------- /src/images/oversize/category_car_b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/oversize/category_car_b.jpg -------------------------------------------------------------------------------- /src/images/oversize/category_car_c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/oversize/category_car_c.jpg -------------------------------------------------------------------------------- /src/images/oversize/category_car_ce.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/oversize/category_car_ce.jpg -------------------------------------------------------------------------------- /src/images/oversize/category_car_d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotMeow/driving-school-frontend/HEAD/src/images/oversize/category_car_d.jpg -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModal.types.ts: -------------------------------------------------------------------------------- 1 | export interface ScheduleTypeInterface { 2 | name: string; 3 | value: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/hooks/useAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useTypedSelector } from "@/hooks/useTypedSelector"; 2 | 3 | export const useAuth = () => useTypedSelector((state) => state.auth.token); 4 | -------------------------------------------------------------------------------- /src/components/other/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | @apply container flex flex-col justify-between h-full; 3 | 4 | .top { 5 | @apply flex flex-col gap-4; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | poweredByHeader: false 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /src/hooks/useTypedDispatch.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { store } from "@/store"; 3 | 4 | type Dispatch = typeof store.dispatch; 5 | export const useTypedDispatch = () => useDispatch(); 6 | -------------------------------------------------------------------------------- /src/components/UI/Logo/Logo.module.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | @apply flex items-center gap-4; 3 | 4 | img { 5 | @apply w-auto h-auto; 6 | } 7 | 8 | > .title { 9 | @apply uppercase text-3xl font-bold; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useTypedSelector.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useSelector } from "react-redux"; 2 | import { TypeRootState } from "@/store"; 3 | 4 | export const useTypedSelector: TypedUseSelectorHook = 5 | useSelector; 6 | -------------------------------------------------------------------------------- /src/components/UI/Select/Select.module.scss: -------------------------------------------------------------------------------- 1 | .select { 2 | @apply space-y-2; 3 | 4 | > h5 { 5 | @apply font-bold; 6 | } 7 | 8 | > select { 9 | @apply text-gray border-gray border rounded-sm pl-2 pr-14 py-2; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/UI/Heading/Heading.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .heading { 4 | @apply text-5xl font-black text-center; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply text-3xl text-left; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/other/Header/HeaderMobile/HeaderMobile.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .mobile { 4 | @apply flex items-center justify-between; 5 | 6 | @media (min-width: $adaptive-min-breakpoint) { 7 | @apply hidden; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/providers/PrivateRouter.types.ts: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | 3 | export type TypeRoles = { 4 | isOnlyUser?: boolean; 5 | }; 6 | 7 | export type NextPageAuth

= NextPage

& TypeRoles; 8 | 9 | export type TypeComponentAuthField = { Component: TypeRoles }; 10 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleCard/ScheduleCard.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply bg-white shadow-md rounded-xl p-6 text-center flex flex-col items-center gap-4; 3 | 4 | > h4 { 5 | @apply font-bold; 6 | } 7 | 8 | > button { 9 | @apply px-8 py-2; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/pages/Profile/ErrorMessage/ErrorMessage.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .error { 4 | @apply bg-white shadow-md flex gap-4 items-center h-16 rounded-lg text-red-500 px-6; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply justify-center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupDeleteModal/GroupDeleteModal.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply space-y-10; 3 | h4 { 4 | @apply text-xl font-bold; 5 | } 6 | 7 | .actions { 8 | @apply flex justify-center gap-4; 9 | 10 | button { 11 | @apply px-12 py-2; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryItem/CategoryItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply flex items-center justify-between; 3 | 4 | p { 5 | @apply text-xl font-bold; 6 | } 7 | 8 | .actions { 9 | @apply flex gap-4; 10 | 11 | > button { 12 | @apply px-6 py-2; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentDeleteModal/StudentDeleteModal.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply space-y-10; 3 | h4 { 4 | @apply text-xl font-bold; 5 | } 6 | 7 | .actions { 8 | @apply flex justify-center gap-4; 9 | 10 | button { 11 | @apply px-12 py-2; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryDeleteModal/CategoryDeleteModal.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply space-y-10; 3 | h4 { 4 | @apply text-xl font-bold; 5 | } 6 | 7 | .actions { 8 | @apply flex justify-center gap-4; 9 | 10 | button { 11 | @apply px-12 py-2; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorDeleteModal/InstructorDeleteModal.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply space-y-10; 3 | h4 { 4 | @apply text-xl font-bold; 5 | } 6 | 7 | .actions { 8 | @apply flex justify-center gap-4; 9 | 10 | button { 11 | @apply px-12 py-2; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModalItem/ScheduleDeleteModal/ScheduleDeleteModal.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply space-y-10; 3 | h4 { 4 | @apply text-xl font-bold; 5 | } 6 | 7 | .actions { 8 | @apply flex justify-center gap-4; 9 | 10 | button { 11 | @apply px-12 py-2; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/other/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | @apply flex items-center justify-between mt-16 py-4 border-t-2 border-gray; 3 | 4 | nav > ul { 5 | @apply flex gap-6; 6 | } 7 | 8 | @media (max-width: 700px) { 9 | @apply flex-col gap-6; 10 | 11 | nav > ul { 12 | @apply flex-col text-center; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/other/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .header { 4 | @apply pt-4 mb-10; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply mb-0; 8 | } 9 | } 10 | 11 | .modal { 12 | width: 480px; 13 | 14 | @media (max-width: $adaptive-max-breakpoint) { 15 | @apply w-full h-full; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/NavigationCard/NavigationCard.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply bg-white shadow-md rounded-xl p-6 flex flex-col gap-4; 3 | 4 | > h4 { 5 | @apply font-bold flex items-center gap-4; 6 | } 7 | 8 | > ul { 9 | @apply space-y-4 font-medium; 10 | 11 | .active { 12 | @apply font-bold; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import Layout from "../components/other/Layout/Layout"; 4 | import Home from "../components/pages/Home/Home"; 5 | 6 | const HomePage: NextPage = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default HomePage; 15 | -------------------------------------------------------------------------------- /src/store/api/auth/auth.types.ts: -------------------------------------------------------------------------------- 1 | export interface RegisterType { 2 | surname: string; 3 | name: string; 4 | patronymic?: string; 5 | phone: string; 6 | email: string; 7 | password: string; 8 | } 9 | 10 | export interface LoginType { 11 | email: string; 12 | password: string; 13 | } 14 | 15 | export interface ResponseType { 16 | token: string | null; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/other/ModalWrapper/ModalWrapper.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply h-screen w-screen fixed top-0 left-0 z-30; 3 | background-color: rgba(0 0 0 / 30%); 4 | 5 | > div { 6 | @apply h-full flex items-start justify-center overflow-y-scroll mt-10; 7 | overflow: auto; 8 | } 9 | 10 | .inner { 11 | @apply bg-white rounded-md px-8 py-6; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupSearch/GroupSearch.module.scss: -------------------------------------------------------------------------------- 1 | .article { 2 | @apply bg-white rounded-xl px-10 flex gap-6 py-8; 3 | 4 | .input { 5 | @apply w-full; 6 | } 7 | 8 | .search { 9 | @apply px-14 py-2; 10 | } 11 | 12 | .filter { 13 | @apply px-6 py-2 gap-2; 14 | 15 | > svg { 16 | @apply w-4; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentSearch/StudentSearch.module.scss: -------------------------------------------------------------------------------- 1 | .article { 2 | @apply bg-white rounded-xl px-10 flex gap-6 py-8; 3 | 4 | .input { 5 | @apply w-full; 6 | } 7 | 8 | .search { 9 | @apply px-14 py-2; 10 | } 11 | 12 | .filter { 13 | @apply px-6 py-2 gap-2; 14 | 15 | > svg { 16 | @apply w-4; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoriesSearch/CategoriesSearch.module.scss: -------------------------------------------------------------------------------- 1 | .article { 2 | @apply bg-white rounded-xl px-10 flex gap-6 py-8; 3 | 4 | .input { 5 | @apply w-full; 6 | } 7 | 8 | .search { 9 | @apply px-14 py-2; 10 | } 11 | 12 | .filter { 13 | @apply px-6 py-2 gap-2; 14 | 15 | > svg { 16 | @apply w-4; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/getScheduleType.utils.ts: -------------------------------------------------------------------------------- 1 | import { ScheduleEnum } from "@/store/api/schedules/schedules.types"; 2 | 3 | export const getScheduleType = (type: ScheduleEnum) => { 4 | switch (type) { 5 | case ScheduleEnum.Practice: 6 | return "Практика"; 7 | case ScheduleEnum.Testing: 8 | return "Тестирование"; 9 | case ScheduleEnum.Theory: 10 | return "Теория"; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorsSearch/InstructorsSearch.module.scss: -------------------------------------------------------------------------------- 1 | .article { 2 | @apply bg-white rounded-xl px-10 flex gap-6 py-8; 3 | 4 | .input { 5 | @apply w-full; 6 | } 7 | 8 | .search { 9 | @apply px-14 py-2; 10 | } 11 | 12 | .filter { 13 | @apply px-6 py-2 gap-2; 14 | 15 | > svg { 16 | @apply w-4; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Profile.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .main { 4 | @apply grid gap-4; 5 | grid-template-columns: 2fr 6fr; 6 | 7 | @media (max-width: $adaptive-max-breakpoint) { 8 | @apply grid-cols-1; 9 | } 10 | 11 | .left { 12 | @apply flex flex-col gap-6; 13 | } 14 | 15 | .body { 16 | ul { 17 | @apply space-y-6; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentCreateModal/StudentCreateModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 800px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorCreateModal/InstructorCreateModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 1000px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/UI/Input/InputSecondary/InputSecondary.module.scss: -------------------------------------------------------------------------------- 1 | .input { 2 | @apply space-y-2; 3 | 4 | > h5 { 5 | @apply text-xl; 6 | } 7 | 8 | > input { 9 | @apply border border-gray bg-inherit outline-none rounded-sm py-2 px-6 w-full; 10 | 11 | &::placeholder { 12 | @apply text-gray; 13 | } 14 | 15 | &.dark { 16 | &:focus { 17 | @apply border-white; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/pages/Profile/ErrorMessage/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./ErrorMessage.module.scss"; 4 | import Error from "@/components/other/Icons/Error"; 5 | 6 | const ErrorMessage: FC = () => { 7 | return ( 8 |

9 | 10 | Вы пока что не записаны на обучение. 11 |
12 | ); 13 | }; 14 | 15 | export default ErrorMessage; 16 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModal.data.ts: -------------------------------------------------------------------------------- 1 | import { ScheduleTypeInterface } from "@/components/pages/Profile/Teacher/ScheduleModal/ScheduleModal.types"; 2 | 3 | export const ScheduleTypes: ScheduleTypeInterface[] = [ 4 | { 5 | name: "Теория", 6 | value: "theory" 7 | }, 8 | { 9 | name: "Практика", 10 | value: "practice" 11 | }, 12 | { 13 | name: "Тестирование", 14 | value: "testing" 15 | } 16 | ]; 17 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Student/TeachersCard/TeachersCard.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply bg-white shadow-md rounded-xl p-6 text-center flex flex-col items-center gap-8; 3 | 4 | > div { 5 | h4 { 6 | @apply font-bold; 7 | } 8 | 9 | p { 10 | @apply font-medium; 11 | } 12 | 13 | a { 14 | @apply block mt-3 text-primary font-medium; 15 | } 16 | } 17 | 18 | .line { 19 | @apply h-0.5 w-full bg-gray opacity-20; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/pages/Profile/UserInfoCard/UserInfoCard.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply bg-white shadow-md flex flex-col items-center text-center justify-center p-10 gap-8 rounded-xl; 3 | 4 | > .info { 5 | @apply flex flex-col items-center gap-4; 6 | h3 { 7 | @apply font-bold text-xl; 8 | } 9 | p { 10 | > span { 11 | @apply text-primary font-bold; 12 | } 13 | } 14 | } 15 | 16 | > button { 17 | @apply px-16 py-2; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useDebounce = (value: T, delay: number): T => { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModalItem/ScheduleModalItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply flex justify-between items-center; 3 | 4 | > .info { 5 | @apply flex items-center gap-4; 6 | 7 | h4 { 8 | @apply text-xl font-bold; 9 | } 10 | } 11 | 12 | .type { 13 | @apply text-xl font-medium; 14 | } 15 | 16 | .action { 17 | @apply flex gap-6 items-center; 18 | 19 | button { 20 | @apply px-8 py-2; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/pages/Contacts/Contacts.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./Contacts.module.scss"; 4 | import QuestionsSection from "../Home/QuestionsSection/QuestionsSection"; 5 | import ContactsSection from "./ContactsSection/ContactsSection"; 6 | 7 | const Contacts: FC = () => { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Contacts; 17 | -------------------------------------------------------------------------------- /src/components/UI/Heading/Heading.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLAttributes, PropsWithChildren } from "react"; 2 | 3 | import styles from "./Heading.module.scss"; 4 | 5 | interface Props extends HTMLAttributes {} 6 | 7 | const Heading: FC> = ({ 8 | children, 9 | className, 10 | ...props 11 | }) => { 12 | return ( 13 |

14 | {children} 15 |

16 | ); 17 | }; 18 | 19 | export default Heading; 20 | -------------------------------------------------------------------------------- /src/components/other/Icons/Burger.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Burger: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default Burger; 21 | -------------------------------------------------------------------------------- /src/components/other/Icons/ExitThin.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const ExitThin: FC = () => { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default ExitThin; 23 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | import { Head, Html, Main, NextScript } from "next/document"; 4 | 5 | const Document: NextPage = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Document; 21 | -------------------------------------------------------------------------------- /src/components/other/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, PropsWithChildren } from "react"; 2 | 3 | import Footer from "../Footer/Footer"; 4 | 5 | import styles from "./Layout.module.scss"; 6 | import Header from "../Header/Header"; 7 | 8 | const Layout: FC = ({ children }) => { 9 | return ( 10 |
11 |
12 |
13 | {children} 14 |
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Layout; 21 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorItem/InstructorItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply grid items-center; 3 | grid-template-columns: 2fr 2fr 1fr; 4 | 5 | .about { 6 | @apply flex items-center gap-4; 7 | 8 | svg { 9 | @apply w-16; 10 | } 11 | 12 | h3 { 13 | @apply text-xl font-bold; 14 | } 15 | } 16 | 17 | .role { 18 | @apply pl-6; 19 | } 20 | 21 | .actions { 22 | @apply flex gap-4; 23 | 24 | > button { 25 | @apply px-6 py-2; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/contacts/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextPage } from "next"; 3 | 4 | import Layout from "@/../../src/components/other/Layout/Layout"; 5 | import Contacts from "@/../../src/components/pages/Contacts/Contacts"; 6 | 7 | import Head from "next/head"; 8 | 9 | const ContactsPage: NextPage = () => { 10 | return ( 11 | 12 | 13 | Driving School - Контакты 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default ContactsPage; 21 | -------------------------------------------------------------------------------- /src/components/UI/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | @apply rounded-sm flex items-center justify-center transition-all; 3 | } 4 | 5 | .primary { 6 | @apply bg-primary text-white; 7 | 8 | &:hover { 9 | @apply bg-primary-active; 10 | } 11 | 12 | &:active { 13 | @apply bg-primary; 14 | } 15 | } 16 | 17 | .secondary { 18 | @apply border border-black text-black; 19 | 20 | &:hover { 21 | @apply text-white bg-secondary-hover; 22 | } 23 | 24 | &:active { 25 | @apply text-white bg-secondary-active; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/UI/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import Image from "next/image"; 4 | 5 | import styles from "./Logo.module.scss"; 6 | import Link from "next/link"; 7 | 8 | import logo from "/public/favicon.png"; 9 | 10 | const Logo: FC = () => { 11 | return ( 12 | 13 | Логотип 14 | Автошкола 15 | 16 | ); 17 | }; 18 | 19 | export default Logo; 20 | -------------------------------------------------------------------------------- /src/components/other/Icons/Cross.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Cross: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default Cross; 21 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # idea 39 | .idea 40 | 41 | # env 42 | .env -------------------------------------------------------------------------------- /src/store/auth/auth.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { ResponseType } from "@/store/api/auth/auth.types"; 3 | 4 | const initialState: ResponseType = { 5 | token: null 6 | }; 7 | 8 | export const authSlice = createSlice({ 9 | name: "auth", 10 | initialState, 11 | reducers: { 12 | logout: (state) => { 13 | state.token = null; 14 | }, 15 | setToken: (state, action: PayloadAction) => { 16 | state.token = action.payload.token; 17 | } 18 | } 19 | }); 20 | 21 | export const { logout, setToken } = authSlice.actions; 22 | -------------------------------------------------------------------------------- /src/store/api/users/users.types.ts: -------------------------------------------------------------------------------- 1 | import { GroupType } from "@/store/api/groups/groups.types"; 2 | 3 | export interface UserType { 4 | id: number; 5 | surname: string; 6 | name: string; 7 | patronymic?: string; 8 | phone: string; 9 | email: string; 10 | password: string; 11 | role: UserRole; 12 | avatarPath?: string; 13 | group: GroupType; 14 | } 15 | 16 | export interface ChangeUserType { 17 | role: string; 18 | } 19 | 20 | export enum UserRole { 21 | ADMIN = "admin", 22 | PRACTICE_TEACHER = "practice_teacher", 23 | THEORY_TEACHER = "theory_teacher", 24 | STUDENT = "student" 25 | } 26 | -------------------------------------------------------------------------------- /src/providers/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { FC, PropsWithChildren } from "react"; 3 | import { TypeComponentAuthField } from "@/providers/PrivateRouter.types"; 4 | 5 | const DynamicCheckRole = dynamic(() => import("./CheckRole"), { 6 | ssr: false 7 | }); 8 | 9 | const AuthProvider: FC> = ({ 10 | Component: { isOnlyUser }, 11 | children 12 | }) => { 13 | return !isOnlyUser ? ( 14 | <>{children} 15 | ) : ( 16 | {children} 17 | ); 18 | }; 19 | 20 | export default AuthProvider; 21 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryChangeModal/CategoryChangeModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 600px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | 18 | .body { 19 | form { 20 | @apply space-y-4; 21 | 22 | label { 23 | @apply w-full mt-8; 24 | } 25 | 26 | button { 27 | @apply px-6 py-2; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryCreateModal/CategoryCreateModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 600px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | 18 | .body { 19 | form { 20 | @apply space-y-4; 21 | 22 | label { 23 | @apply w-full mt-8; 24 | } 25 | 26 | button { 27 | @apply px-6 py-2; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/other/Header/HeaderDesktop/HeaderDesktop.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .desktop { 4 | @apply flex items-center justify-between; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply hidden; 8 | } 9 | 10 | > .about { 11 | @apply flex items-center gap-4; 12 | > .navigation { 13 | @apply flex items-center pl-4 h-8; 14 | border-left: 1px solid rgba(0 0 0 / 30%); 15 | 16 | > ul { 17 | @apply flex gap-6 text-lg; 18 | } 19 | } 20 | } 21 | 22 | > .actions { 23 | @apply flex gap-4; 24 | button { 25 | @apply px-6 py-2; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/pages/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./Home.module.scss"; 4 | import CategoriesSection from "./CategoriesSection/CategoriesSection"; 5 | import FormSection from "./FormSection/FormSection"; 6 | import PrimarySection from "./PrimarySection/PrimarySection"; 7 | import QuestionsSection from "./QuestionsSection/QuestionsSection"; 8 | 9 | const Home: FC = () => { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Home; 21 | -------------------------------------------------------------------------------- /src/components/pages/Home/QuestionsSection/QuestionsSection.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | .questions { 3 | @apply mt-10; 4 | article { 5 | @apply border-b border-gray py-4; 6 | .heading { 7 | @apply flex justify-between items-center cursor-pointer; 8 | 9 | h3 { 10 | @apply text-4xl font-bold; 11 | } 12 | 13 | button { 14 | @apply bg-primary rounded-full text-white p-2 transition-all; 15 | 16 | &.rotate { 17 | @apply rotate-45; 18 | } 19 | } 20 | } 21 | 22 | &:first-child { 23 | @apply border-t; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentItem/StudentItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply flex items-center justify-between; 3 | 4 | .about { 5 | @apply flex items-center gap-4; 6 | 7 | svg { 8 | @apply w-16; 9 | } 10 | 11 | .info { 12 | h3 { 13 | @apply text-xl font-bold; 14 | } 15 | p { 16 | span { 17 | @apply text-primary font-bold; 18 | } 19 | } 20 | } 21 | } 22 | 23 | .teachers { 24 | @apply flex items-center gap-10; 25 | h4 { 26 | @apply font-bold; 27 | } 28 | } 29 | 30 | > button { 31 | @apply px-6 py-2; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/store/api/groups/groups.types.ts: -------------------------------------------------------------------------------- 1 | import { ScheduleType } from "@/store/api/schedules/schedules.types"; 2 | import { UserType } from "@/store/api/users/users.types"; 3 | import { CategoryType } from "@/store/api/categories/categories.types"; 4 | 5 | export interface GroupType { 6 | id: number; 7 | practiceTeacher: UserType; 8 | theoryTeacher: UserType; 9 | category: CategoryType; 10 | students: UserType[]; 11 | schedules: ScheduleType[]; 12 | } 13 | 14 | export interface CreateGroupType { 15 | practiceTeacherId: number; 16 | theoryTeacherId: number; 17 | categoryId: number; 18 | } 19 | 20 | export interface ChangeGroupType extends Partial {} 21 | -------------------------------------------------------------------------------- /src/providers/CheckRole.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { FC, PropsWithChildren } from "react"; 3 | 4 | import { useAuth } from "@/hooks/useAuth"; 5 | import { TypeComponentAuthField } from "@/providers/PrivateRouter.types"; 6 | 7 | const CheckRole: FC> = ({ 8 | children, 9 | Component: { isOnlyUser } 10 | }) => { 11 | const token = useAuth(); 12 | const { replace, pathname } = useRouter(); 13 | 14 | const Children = () => <>{children}; 15 | 16 | if (token) return ; 17 | 18 | if (isOnlyUser) pathname !== "/" && replace("/"); 19 | return null; 20 | }; 21 | 22 | export default CheckRole; 23 | -------------------------------------------------------------------------------- /src/components/other/ModalWrapper/LoginModal/LoginModal.module.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | @apply overflow-y-scroll h-full px-4; 3 | .top { 4 | @apply flex justify-between items-center mb-10; 5 | 6 | h4 { 7 | @apply text-2xl font-black; 8 | } 9 | } 10 | 11 | .primary { 12 | @apply relative flex flex-col gap-14; 13 | .auth { 14 | @apply self-start px-20 py-3 mb-6; 15 | } 16 | .register { 17 | @apply px-10 py-3 mb-6; 18 | } 19 | } 20 | 21 | .footer { 22 | a, 23 | button { 24 | @apply text-left cursor-pointer text-primary font-medium; 25 | } 26 | 27 | .info { 28 | @apply text-base mt-4; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupsSection.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | @apply space-y-4; 3 | > div { 4 | @apply bg-white rounded-xl shadow-md px-10 py-6 flex flex-col; 5 | > div { 6 | @apply flex gap-4 items-center; 7 | h2 { 8 | @apply text-xl font-bold; 9 | } 10 | button { 11 | @apply text-base px-4 py-1; 12 | } 13 | } 14 | > ul { 15 | > li { 16 | @apply border-b py-6; 17 | border-color: #b9b9b9; 18 | 19 | &:last-child { 20 | @apply border-none; 21 | } 22 | } 23 | } 24 | } 25 | 26 | .error { 27 | @apply text-red-500 flex gap-4 pt-8; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/store/api/schedules/schedules.types.ts: -------------------------------------------------------------------------------- 1 | import { GroupType } from "@/store/api/groups/groups.types"; 2 | 3 | export interface ScheduleType { 4 | id: number; 5 | type: ScheduleEnum; 6 | startTime: string; 7 | endTime: string; 8 | date: string; 9 | address?: string; 10 | group: GroupType; 11 | } 12 | 13 | export interface CreateScheduleType { 14 | type: ScheduleEnum; 15 | startTime: string; 16 | endTime: string; 17 | date: string; 18 | address?: string; 19 | } 20 | 21 | export interface UpdateScheduleType extends Partial {} 22 | 23 | export enum ScheduleEnum { 24 | Theory = "theory", 25 | Practice = "practice", 26 | Testing = "testing" 27 | } 28 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentsSection.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | @apply space-y-4; 3 | > div { 4 | @apply bg-white rounded-xl shadow-md px-10 py-6 flex flex-col; 5 | > div { 6 | @apply flex gap-4 items-center; 7 | h2 { 8 | @apply text-xl font-bold; 9 | } 10 | button { 11 | @apply text-base px-4 py-1; 12 | } 13 | } 14 | > ul { 15 | > li { 16 | @apply border-b py-6; 17 | border-color: #b9b9b9; 18 | 19 | &:last-child { 20 | @apply border-none; 21 | } 22 | } 23 | } 24 | } 25 | 26 | .error { 27 | @apply text-red-500 flex gap-4 pt-8; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoriesSection.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | @apply space-y-4; 3 | > div { 4 | @apply bg-white rounded-xl shadow-md px-10 py-6 flex flex-col; 5 | > div { 6 | @apply flex gap-4 items-center; 7 | h2 { 8 | @apply text-xl font-bold; 9 | } 10 | button { 11 | @apply text-base px-4 py-1; 12 | } 13 | } 14 | > ul { 15 | > li { 16 | @apply border-b py-6; 17 | border-color: #b9b9b9; 18 | 19 | &:last-child { 20 | @apply border-none; 21 | } 22 | } 23 | } 24 | } 25 | 26 | .error { 27 | @apply text-red-500 flex gap-4 pt-8; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorsSection.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | @apply space-y-4; 3 | > div { 4 | @apply bg-white rounded-xl shadow-md px-10 py-6 flex flex-col; 5 | > div { 6 | @apply flex gap-4 items-center; 7 | h2 { 8 | @apply text-xl font-bold; 9 | } 10 | button { 11 | @apply text-base px-4 py-1; 12 | } 13 | } 14 | > ul { 15 | > li { 16 | @apply border-b py-6; 17 | border-color: #b9b9b9; 18 | 19 | &:last-child { 20 | @apply border-none; 21 | } 22 | } 23 | } 24 | } 25 | 26 | .error { 27 | @apply text-red-500 flex gap-4 pt-8; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Layout from "@/../../src/components/other/Layout/Layout"; 3 | import { NextPageAuth } from "@/providers/PrivateRouter.types"; 4 | import Head from "next/head"; 5 | import { api } from "@/store/api/api"; 6 | import Profile from "@/components/pages/Profile/Profile"; 7 | 8 | const ProfilePage: NextPageAuth = () => { 9 | const { data } = api.useGetAuthUserQuery(); 10 | 11 | return ( 12 | 13 | 14 | Driving School - Профиль 15 | 16 | {data && } 17 | 18 | ); 19 | }; 20 | 21 | ProfilePage.isOnlyUser = true; 22 | 23 | export default ProfilePage; 24 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupItem/GroupItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply flex items-center justify-between; 3 | 4 | .about { 5 | @apply flex items-center gap-4; 6 | 7 | svg { 8 | @apply w-20; 9 | } 10 | 11 | .info { 12 | h3 { 13 | @apply text-xl font-bold; 14 | } 15 | p { 16 | span { 17 | @apply text-primary font-bold; 18 | } 19 | } 20 | } 21 | } 22 | 23 | .teachers { 24 | @apply flex flex-col items-center text-center gap-2; 25 | h4 { 26 | @apply font-bold; 27 | } 28 | } 29 | 30 | .actions { 31 | @apply flex gap-4; 32 | 33 | > button { 34 | @apply px-6 py-2; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Student/StudentScheduleItem/StudentScheduleItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply bg-white rounded-lg p-6 shadow-md; 3 | 4 | header { 5 | @apply flex items-center justify-between; 6 | 7 | h3 { 8 | @apply text-2xl font-bold; 9 | } 10 | 11 | p { 12 | @apply text-gray font-medium mt-1; 13 | } 14 | 15 | button { 16 | @apply transition-all duration-300; 17 | } 18 | 19 | .active { 20 | @apply rotate-180; 21 | } 22 | } 23 | 24 | footer { 25 | .address { 26 | @apply flex items-center gap-2 text-xl font-medium bg-notification py-5 px-8 rounded-full; 27 | 28 | span { 29 | @apply flex items-center gap-2 text-2xl font-bold; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/TeacherScheduleItem/TeacherScheduleItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply bg-white rounded-lg p-6 shadow-md; 3 | 4 | header { 5 | @apply flex items-center justify-between; 6 | 7 | h3 { 8 | @apply text-2xl font-bold; 9 | } 10 | 11 | p { 12 | @apply text-gray font-medium mt-1; 13 | } 14 | 15 | button { 16 | @apply transition-all duration-300; 17 | } 18 | 19 | .active { 20 | @apply rotate-180; 21 | } 22 | } 23 | 24 | footer { 25 | .address { 26 | @apply flex items-center gap-2 text-xl font-medium bg-notification py-5 px-8 rounded-full; 27 | 28 | span { 29 | @apply flex items-center gap-2 text-2xl font-bold; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/components/other/Icons/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Arrow: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default Arrow; 21 | -------------------------------------------------------------------------------- /src/components/other/Icons/ListArrow.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const ListArrow: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default ListArrow; 21 | -------------------------------------------------------------------------------- /src/components/other/Icons/LongArrow.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const LongArrow: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default LongArrow; 21 | -------------------------------------------------------------------------------- /src/components/other/ModalWrapper/RegisterModal/RegisterModal.module.scss: -------------------------------------------------------------------------------- 1 | .register { 2 | @apply overflow-y-scroll h-full px-4; 3 | .top { 4 | @apply flex justify-between items-center mb-10; 5 | 6 | h4 { 7 | @apply text-2xl font-black; 8 | } 9 | } 10 | 11 | .primary { 12 | @apply relative flex flex-col gap-14; 13 | .auth { 14 | @apply px-20 py-3 mb-6; 15 | } 16 | .register { 17 | @apply self-start px-5 py-3 mb-6; 18 | } 19 | .miss { 20 | @apply absolute text-gray right-0; 21 | bottom: 88px; 22 | } 23 | } 24 | 25 | .footer { 26 | a, 27 | button { 28 | @apply text-left cursor-pointer text-primary font-medium; 29 | } 30 | 31 | .info { 32 | @apply text-base mt-4; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "baseUrl": "./", 22 | "paths": { 23 | "@/*": [ 24 | "./src/*" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import Student from "@/components/pages/Profile/Student/Student"; 3 | import Teacher from "@/components/pages/Profile/Teacher/Teacher"; 4 | import Admin from "@/components/pages/Profile/Admin/Admin"; 5 | import { UserRole, UserType } from "@/store/api/users/users.types"; 6 | 7 | interface Props { 8 | user: UserType; 9 | } 10 | 11 | const Profile: FC = ({ user }) => { 12 | return ( 13 | <> 14 | {user.role === UserRole.STUDENT && } 15 | {(user.role === UserRole.THEORY_TEACHER || 16 | user.role === UserRole.PRACTICE_TEACHER) && } 17 | {user.role === UserRole.ADMIN && } 18 | 19 | ); 20 | }; 21 | 22 | export default Profile; 23 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorChangeModal/InstructorChangeModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 400px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | 18 | .body { 19 | @apply my-4; 20 | .info { 21 | @apply flex items-center gap-4; 22 | 23 | h4 { 24 | @apply text-2xl font-bold; 25 | } 26 | 27 | p { 28 | @apply text-xl; 29 | } 30 | } 31 | 32 | form { 33 | @apply my-4 space-y-4; 34 | 35 | button { 36 | @apply px-10 py-2 mt-4; 37 | } 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupCreateModal/GroupCreateModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 900px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | 18 | .body { 19 | .group { 20 | @apply flex items-center gap-4; 21 | 22 | h4 { 23 | @apply font-bold; 24 | } 25 | svg { 26 | @apply w-16; 27 | } 28 | } 29 | 30 | form { 31 | @apply space-y-4; 32 | > div { 33 | @apply flex flex-wrap gap-4; 34 | } 35 | 36 | button { 37 | @apply px-6 py-2; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentCreateModal/StudentCreateItem/StudentCreateItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply flex items-center justify-between; 3 | 4 | .about { 5 | @apply flex items-center gap-4; 6 | 7 | svg { 8 | @apply w-16; 9 | } 10 | 11 | .info { 12 | h3 { 13 | @apply text-xl font-bold; 14 | } 15 | p { 16 | span { 17 | @apply text-primary font-bold; 18 | } 19 | } 20 | } 21 | } 22 | 23 | .teachers { 24 | @apply flex flex-col items-center text-center gap-2; 25 | h4 { 26 | @apply font-bold; 27 | } 28 | } 29 | 30 | .actions { 31 | @apply flex items-center gap-4; 32 | 33 | label { 34 | @apply flex items-center gap-4 space-y-0; 35 | } 36 | 37 | > button { 38 | @apply px-6 py-2; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/UI/Radio/Radio.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, forwardRef, InputHTMLAttributes } from "react"; 2 | 3 | import styles from "./Radio.module.scss"; 4 | import classNames from "classnames"; 5 | 6 | interface Props extends InputHTMLAttributes { 7 | title: string; 8 | dark?: boolean; 9 | required?: boolean; 10 | } 11 | 12 | const Radio: FC = forwardRef( 13 | ({ title, dark = false, required = false, ...props }, ref) => { 14 | return ( 15 | 24 | ); 25 | } 26 | ); 27 | 28 | Radio.displayName = "Radio"; 29 | 30 | export default Radio; 31 | -------------------------------------------------------------------------------- /src/store/api/api.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | import { TypeRootState } from "@/store"; 3 | import { UserType } from "@/store/api/users/users.types"; 4 | export const api = createApi({ 5 | reducerPath: "api", 6 | baseQuery: fetchBaseQuery({ 7 | baseUrl: process.env.NEXT_PUBLIC_API_URL, 8 | prepareHeaders: (headers, { getState }) => { 9 | const token = (getState() as TypeRootState).auth.token; 10 | if (token) headers.set("Authorization", `Bearer ${token}`); 11 | return headers; 12 | } 13 | }), 14 | tagTypes: ["Profile", "Category", "Group", "Schedule", "User"], 15 | endpoints: (builder) => ({ 16 | getAuthUser: builder.query({ 17 | query: () => `/users/profile`, 18 | forceRefetch: () => true, 19 | providesTags: ["Profile"] 20 | }) 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/UI/Input/InputPrimary/InputPrimary.module.scss: -------------------------------------------------------------------------------- 1 | .input { 2 | @apply relative text-gray-black; 3 | > label { 4 | @apply relative flex flex-col; 5 | > span { 6 | @apply absolute -top-6 transition-all cursor-text peer-placeholder-shown:text-gray; 7 | } 8 | 9 | > input { 10 | @apply py-1 text-xl placeholder-transparent bg-transparent border-b border-gray focus:outline-none w-full; 11 | } 12 | } 13 | } 14 | 15 | .error { 16 | > label > span { 17 | @apply text-red-700 peer-placeholder-shown:text-red-700; 18 | } 19 | 20 | > label > input { 21 | @apply border-red-700; 22 | } 23 | } 24 | 25 | .message { 26 | @apply absolute text-sm mt-2 text-red-700; 27 | -webkit-touch-callout: none; 28 | -webkit-user-select: none; 29 | -khtml-user-select: none; 30 | -moz-user-select: none; 31 | -ms-user-select: none; 32 | user-select: none; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorCreateModal/InstructorCreateItem/InstructorCreateItem.module.scss: -------------------------------------------------------------------------------- 1 | .item { 2 | @apply grid items-center; 3 | grid-template-columns: 2fr 3fr 1fr; 4 | 5 | .about { 6 | @apply flex items-center gap-4; 7 | 8 | svg { 9 | @apply w-16; 10 | } 11 | 12 | .info { 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | p { 17 | span { 18 | @apply text-primary font-bold; 19 | } 20 | } 21 | } 22 | } 23 | 24 | .teachers { 25 | @apply flex flex-col items-center text-center gap-2; 26 | h4 { 27 | @apply font-bold; 28 | } 29 | } 30 | 31 | .actions { 32 | @apply flex items-center gap-4; 33 | 34 | label { 35 | @apply flex items-center gap-4 space-y-0; 36 | } 37 | } 38 | 39 | > button { 40 | @apply px-6 py-2; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/other/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./Footer.module.scss"; 4 | import Logo from "../../UI/Logo/Logo"; 5 | import Link from "next/link"; 6 | 7 | const Footer: FC = () => { 8 | return ( 9 |
10 | 11 | 30 |
31 | ); 32 | }; 33 | 34 | export default Footer; 35 | -------------------------------------------------------------------------------- /src/components/other/Header/HeaderMobile/Dropdown/Dropdown.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .overlay { 4 | @apply fixed opacity-0 pointer-events-none w-full h-full left-0 top-0 z-20 transition-all; 5 | background-color: rgba(0 0 0 / 30%); 6 | 7 | @media (min-width: $adaptive-min-breakpoint) { 8 | @apply hidden; 9 | } 10 | } 11 | 12 | .show { 13 | @apply opacity-100 pointer-events-auto; 14 | 15 | > .dropdown { 16 | @apply top-0; 17 | } 18 | } 19 | 20 | .dropdown { 21 | @apply px-4 py-4 fixed z-40 -top-96 left-0 bg-white w-full text-center flex flex-col gap-6 transition-all duration-700; 22 | 23 | .header { 24 | @apply flex justify-between items-center; 25 | } 26 | 27 | .navigation { 28 | > ul { 29 | @apply flex flex-col gap-6 text-lg; 30 | } 31 | } 32 | 33 | .actions { 34 | @apply flex flex-col items-center gap-4; 35 | button { 36 | @apply px-6 py-2; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/UI/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, forwardRef } from "react"; 2 | import classNames from "classnames"; 3 | 4 | import styles from "./Button.module.scss"; 5 | 6 | interface Props extends ButtonHTMLAttributes { 7 | primary?: boolean; 8 | secondary?: boolean; 9 | } 10 | 11 | const Button = forwardRef( 12 | ( 13 | { children, className, primary = false, secondary = false, ...props }, 14 | ref 15 | ) => { 16 | return ( 17 | 31 | ); 32 | } 33 | ); 34 | 35 | Button.displayName = "Button"; 36 | 37 | export default Button; 38 | -------------------------------------------------------------------------------- /src/components/other/Header/HeaderMobile/HeaderMobile.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./HeaderMobile.module.scss"; 4 | import Logo from "../../../UI/Logo/Logo"; 5 | import Burger from "../../Icons/Burger"; 6 | import dynamic from "next/dynamic"; 7 | 8 | const Dropdown = dynamic(import("./Dropdown/Dropdown"), { 9 | ssr: false 10 | }); 11 | 12 | interface Props { 13 | setIsModalShow: React.Dispatch>; 14 | } 15 | 16 | const HeaderMobile: FC = ({ setIsModalShow }) => { 17 | const [isDropdown, setIsDropdown] = useState(false); 18 | 19 | return ( 20 |
21 | 22 | 25 | 30 |
31 | ); 32 | }; 33 | 34 | export default HeaderMobile; 35 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 1000px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | 18 | .body { 19 | .actions { 20 | @apply mt-4 space-y-4; 21 | 22 | .selects { 23 | @apply flex gap-4; 24 | } 25 | 26 | .inputs { 27 | @apply flex gap-4 flex-wrap; 28 | 29 | label { 30 | @apply w-full; 31 | 32 | input { 33 | @apply w-full; 34 | } 35 | } 36 | } 37 | } 38 | 39 | button { 40 | @apply px-6 py-2 mt-6; 41 | } 42 | } 43 | 44 | .schedule { 45 | @apply mt-6; 46 | > h4 { 47 | @apply font-bold text-xl; 48 | } 49 | 50 | > ul { 51 | @apply space-y-6 mt-6; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/other/Icons/Mail.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Mail: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default Mail; 21 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupChangeModal/GroupChangeModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .modal { 4 | width: 900px; 5 | 6 | @media (max-width: $adaptive-max-breakpoint) { 7 | @apply w-full h-full; 8 | } 9 | 10 | header { 11 | @apply flex items-center justify-between; 12 | 13 | h3 { 14 | @apply text-xl font-bold; 15 | } 16 | } 17 | 18 | .body { 19 | @apply my-4; 20 | .info { 21 | @apply flex items-center gap-4; 22 | 23 | h4 { 24 | @apply text-2xl font-bold; 25 | } 26 | 27 | p { 28 | @apply text-xl; 29 | 30 | span { 31 | @apply text-primary font-bold; 32 | } 33 | } 34 | } 35 | 36 | form { 37 | @apply my-4; 38 | .selects { 39 | @apply flex flex-wrap gap-6; 40 | } 41 | 42 | button { 43 | @apply px-10 py-2 mt-4; 44 | } 45 | } 46 | 47 | .students { 48 | @apply mt-10; 49 | h4 { 50 | @apply text-xl font-bold; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/UI/Input/InputSecondary/InputSecondary.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, InputHTMLAttributes } from "react"; 2 | 3 | import styles from "./InputSecondary.module.scss"; 4 | import classNames from "classnames"; 5 | import InputSecondaryPhone from "./InputSecondaryPhone/InputSecondaryPhone"; 6 | 7 | interface Props extends InputHTMLAttributes { 8 | title: string; 9 | dark?: boolean; 10 | } 11 | 12 | const InputSecondary = forwardRef( 13 | ({ title, dark = false, ...props }, ref) => { 14 | return ( 15 | 29 | ); 30 | } 31 | ); 32 | 33 | InputSecondary.displayName = "InputSecondary"; 34 | 35 | export default InputSecondary; 36 | -------------------------------------------------------------------------------- /src/store/api/users/users.api.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/store/api/api"; 2 | import { 3 | ChangeUserType, 4 | UserRole, 5 | UserType 6 | } from "@/store/api/users/users.types"; 7 | 8 | export const usersApi = api.injectEndpoints({ 9 | endpoints: (builder) => ({ 10 | getUsers: builder.query< 11 | UserType[], 12 | { search?: string; role?: UserRole; withGroup?: boolean } 13 | >({ 14 | query: (args) => ({ 15 | url: `/users`, 16 | params: { ...args } 17 | }), 18 | forceRefetch: () => true, 19 | providesTags: ["User"] 20 | }), 21 | getUserById: builder.query({ 22 | query: (args) => `/users/${args.userId}`, 23 | providesTags: ["User"] 24 | }), 25 | changeUserRole: builder.mutation< 26 | UserType, 27 | { userId: number; body: ChangeUserType } 28 | >({ 29 | query: (args) => ({ 30 | url: `/users/${args.userId}`, 31 | method: "PATCH", 32 | body: { ...args.body } 33 | }), 34 | invalidatesTags: ["User"] 35 | }) 36 | }) 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/other/Icons/Geo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Geo: FC = () => { 4 | return ( 5 | 12 | 19 | 26 | 27 | ); 28 | }; 29 | 30 | export default Geo; 31 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import { authSlice } from "@/store/auth/auth.slice"; 3 | import storage from "redux-persist/lib/storage"; 4 | import { 5 | FLUSH, 6 | PAUSE, 7 | PERSIST, 8 | persistReducer, 9 | persistStore, 10 | PURGE, 11 | REGISTER, 12 | REHYDRATE 13 | } from "redux-persist"; 14 | import { api } from "@/store/api/api"; 15 | 16 | const persistConfig = { 17 | key: "root", 18 | storage, 19 | whiteList: ["auth"] 20 | }; 21 | 22 | const rootReducer = combineReducers({ 23 | [api.reducerPath]: api.reducer, 24 | auth: authSlice.reducer 25 | }); 26 | 27 | const persistedReducer = persistReducer(persistConfig, rootReducer); 28 | 29 | export const store = configureStore({ 30 | reducer: persistedReducer, 31 | middleware: (getDefaultMiddleware) => 32 | getDefaultMiddleware({ 33 | serializableCheck: { 34 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] 35 | } 36 | }).concat(api.middleware) 37 | }); 38 | 39 | export const persistor = persistStore(store); 40 | export type TypeRootState = ReturnType; 41 | -------------------------------------------------------------------------------- /src/components/other/Icons/Add.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Add: FC = () => { 4 | return ( 5 | 12 | 20 | 28 | 34 | 40 | 41 | ); 42 | }; 43 | 44 | export default Add; 45 | -------------------------------------------------------------------------------- /src/components/other/Icons/Phone.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Phone: FC = () => { 4 | return ( 5 | 12 | 19 | 20 | ); 21 | }; 22 | 23 | export default Phone; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Driving School Frontend

2 | 3 | ![Screen Shot](https://github.com/ShotMeow/ShotMeow/blob/main/assets/driving-school/preview.png?raw=true) 4 | 5 |

О проекте

6 |

Driving School Frontend - веб-интерфейс, осуществленный посредством использования технологий Next.js + TypeScript. Данный проект не продуктовый и разработан в качестве работы для портфолио.

7 | 8 |

Стэк технологий

9 | 17 | 18 |

Запуск репозитория

19 | 20 | ```bash 21 | npm install // Установка необходимых пакетов 22 | 23 | npm run dev // Запуск сервера разработки 24 | ``` 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driving-school-frontend", 3 | "version": "0.8.5", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prettier": "prettier --write ./src" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^2.9.10", 14 | "@reduxjs/toolkit": "^1.9.2", 15 | "classnames": "^2.3.2", 16 | "focus-trap": "^7.2.0", 17 | "framer-motion": "^8.4.0", 18 | "next": "13.1.1", 19 | "nextjs-progressbar": "^0.0.16", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-hook-form": "^7.42.1", 23 | "react-redux": "^8.0.5", 24 | "redux-persist": "^6.0.0", 25 | "sass": "^1.57.1", 26 | "swiper": "^8.4.5", 27 | "yup": "^0.32.11" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^18.11.18", 31 | "@types/react": "18.0.26", 32 | "@types/react-dom": "18.0.10", 33 | "@types/redux-persist": "^4.3.1", 34 | "autoprefixer": "^10.4.13", 35 | "eslint": "8.33.0", 36 | "eslint-config-next": "13.1.6", 37 | "postcss": "^8.4.20", 38 | "prettier": "^2.8.2", 39 | "tailwindcss": "^3.2.4", 40 | "typescript": "4.9.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Student/TeachersCard/TeachersCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./TeachersCard.module.scss"; 4 | import { UserType } from "@/store/api/users/users.types"; 5 | 6 | interface Props { 7 | theoryTeacher: UserType; 8 | practiceTeacher: UserType; 9 | } 10 | 11 | const TeachersCard: FC = ({ theoryTeacher, practiceTeacher }) => { 12 | return ( 13 | 32 | ); 33 | }; 34 | 35 | export default TeachersCard; 36 | -------------------------------------------------------------------------------- /src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700;900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | html { 8 | scroll-behavior: smooth; 9 | } 10 | 11 | body { 12 | @apply font-montserrat text-black; 13 | background-color: #f9f9f9; 14 | } 15 | 16 | html, 17 | body, 18 | #__next { 19 | @apply h-full; 20 | } 21 | 22 | input:-webkit-autofill { 23 | -webkit-box-shadow: 0 0 0 1000px white inset; 24 | } 25 | 26 | @media (max-width: 1200px) { 27 | .--prevent-scroll { 28 | @apply overflow-hidden; 29 | } 30 | } 31 | 32 | *::-webkit-scrollbar { 33 | width: 4px; 34 | } 35 | 36 | *::-webkit-scrollbar-track { 37 | border-radius: 0; 38 | background-color: rgba(0, 0, 0, 0); 39 | } 40 | 41 | *::-webkit-scrollbar-track:hover { 42 | background-color: rgba(0, 0, 0, 0); 43 | } 44 | 45 | *::-webkit-scrollbar-track:active { 46 | background-color: rgba(0, 0, 0, 0); 47 | } 48 | 49 | *::-webkit-scrollbar-thumb { 50 | border-radius: 0; 51 | background-color: #0075ff; 52 | } 53 | 54 | *::-webkit-scrollbar-thumb:hover { 55 | background-color: #2f8fff; 56 | } 57 | 58 | *::-webkit-scrollbar-thumb:active { 59 | background-color: #2f8fff; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/other/Icons/Error.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Error: FC = () => { 4 | return ( 5 | 12 | 17 | 18 | ); 19 | }; 20 | 21 | export default Error; 22 | -------------------------------------------------------------------------------- /src/components/other/Icons/Location.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Location: FC = () => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default Location; 21 | -------------------------------------------------------------------------------- /src/store/api/auth/auth.api.ts: -------------------------------------------------------------------------------- 1 | import {api} from "@/store/api/api"; 2 | import {logout, setToken} from "@/store/auth/auth.slice"; 3 | import {LoginType, RegisterType, ResponseType} from "@/store/api/auth/auth.types"; 4 | 5 | export const authApi = api.injectEndpoints({ 6 | endpoints: (builder) => ({ 7 | register: builder.mutation({ 8 | query: (body) => ({ 9 | url: "/auth/register", 10 | method: "POST", 11 | body: body 12 | }), 13 | invalidatesTags: ["Profile"], 14 | async onQueryStarted(args, { dispatch, queryFulfilled }) { 15 | try { 16 | const { data } = await queryFulfilled; 17 | await dispatch(setToken(data)); 18 | } catch (error) { 19 | await dispatch(logout()); 20 | } 21 | } 22 | }), 23 | login: builder.mutation({ 24 | query: (body) => ({ 25 | url: "/auth/login", 26 | method: "POST", 27 | body: body 28 | }), 29 | invalidatesTags: ["Profile"], 30 | async onQueryStarted(args, { dispatch, queryFulfilled }) { 31 | try { 32 | const { data } = await queryFulfilled; 33 | await dispatch(setToken(data)); 34 | } catch (error) { 35 | await dispatch(logout()); 36 | } 37 | } 38 | }), 39 | }) 40 | }) -------------------------------------------------------------------------------- /src/components/pages/Contacts/ContactsSection/ContactsSection.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .section { 4 | @apply grid gap-8; 5 | grid-template-columns: 5fr 2fr; 6 | 7 | .form, 8 | .info { 9 | @apply bg-white shadow-md rounded-xl p-8; 10 | } 11 | 12 | .form { 13 | @apply flex flex-col justify-between gap-6; 14 | .heading { 15 | @apply text-3xl font-black mb-2; 16 | } 17 | 18 | > form { 19 | @apply flex flex-col gap-8; 20 | > div { 21 | @apply grid gap-6; 22 | grid-template-columns: 1fr 1fr; 23 | } 24 | 25 | button { 26 | @apply px-4 py-2 gap-2; 27 | } 28 | } 29 | } 30 | 31 | .info { 32 | h5 { 33 | @apply text-lg font-black mb-4; 34 | } 35 | 36 | ul { 37 | @apply space-y-4; 38 | li { 39 | @apply flex flex-col; 40 | 41 | span:first-child { 42 | @apply flex items-center gap-2 text-sm font-bold text-black pl-0; 43 | } 44 | 45 | span { 46 | @apply text-sm text-gray pl-7; 47 | } 48 | } 49 | } 50 | } 51 | 52 | @media (max-width: $adaptive-max-breakpoint) { 53 | grid-template-columns: 1fr; 54 | 55 | .form, 56 | .info { 57 | @apply p-4; 58 | } 59 | 60 | .form > form > div { 61 | grid-template-columns: 1fr; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupSearch/GroupSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | 3 | import styles from "./GroupSearch.module.scss"; 4 | import InputPrimary from "@/components/UI/Input/InputPrimary/InputPrimary"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import { useDebounce } from "@/hooks/useDebounce"; 7 | import { groupsApi } from "@/store/api/groups/groups.api"; 8 | import { GroupType } from "@/store/api/groups/groups.types"; 9 | 10 | interface Props { 11 | setGroups: React.Dispatch>; 12 | } 13 | 14 | const GroupSearch: FC = ({ setGroups }) => { 15 | const [value, setValue] = useState(""); 16 | const debounce = useDebounce(value, 500); 17 | 18 | const { data } = groupsApi.useGetGroupsQuery({ search: debounce }); 19 | 20 | useEffect(() => { 21 | data && setGroups(data); 22 | }, [data, setGroups]); 23 | 24 | return ( 25 |
26 | setValue(event.target.value)} 29 | className={styles.input} 30 | title="Название группы" 31 | /> 32 | 35 |
36 | ); 37 | }; 38 | 39 | export default GroupSearch; 40 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupDeleteModal/GroupDeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import Button from "@/components/UI/Button/Button"; 4 | 5 | import styles from "./GroupDeleteModal.module.scss"; 6 | import { groupsApi } from "@/store/api/groups/groups.api"; 7 | 8 | interface Props { 9 | modalShown: boolean; 10 | setModalShown: React.Dispatch>; 11 | groupId: number; 12 | } 13 | 14 | const GroupDeleteModal: FC = ({ 15 | modalShown, 16 | setModalShown, 17 | groupId 18 | }) => { 19 | const [deleteGroup] = groupsApi.useDeleteGroupMutation(); 20 | 21 | const handleSubmit = () => { 22 | deleteGroup({ groupId: groupId }).then(() => setModalShown(false)); 23 | }; 24 | 25 | return ( 26 | 31 |

Вы уверены что хотите удалить группу?

32 |
33 | 36 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default GroupDeleteModal; 45 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentSearch/StudentSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | 3 | import styles from "./StudentSearch.module.scss"; 4 | import InputPrimary from "@/components/UI/Input/InputPrimary/InputPrimary"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import { useDebounce } from "@/hooks/useDebounce"; 7 | import { UserType } from "@/store/api/users/users.types"; 8 | import { usersApi } from "@/store/api/users/users.api"; 9 | 10 | interface Props { 11 | setStudents: React.Dispatch>; 12 | } 13 | 14 | const StudentSearch: FC = ({ setStudents }) => { 15 | const [value, setValue] = useState(""); 16 | const debounce = useDebounce(value, 500); 17 | 18 | const { data } = usersApi.useGetUsersQuery({ 19 | search: debounce, 20 | withGroup: true 21 | }); 22 | 23 | useEffect(() => { 24 | data && setStudents(data); 25 | }, [data, setStudents]); 26 | 27 | return ( 28 |
29 | setValue(event.target.value)} 32 | className={styles.input} 33 | title="Имя ученика" 34 | /> 35 | 38 |
39 | ); 40 | }; 41 | 42 | export default StudentSearch; 43 | -------------------------------------------------------------------------------- /src/components/pages/Home/FormSection/FormSection.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .section { 4 | .form { 5 | @apply relative bg-black rounded-xl p-10 text-white flex gap-6 mt-10 shadow-md; 6 | 7 | .inputs { 8 | @apply flex flex-col gap-2; 9 | width: 340px; 10 | } 11 | 12 | .buttons { 13 | @apply border-l border-gray px-6 space-y-10; 14 | h5 { 15 | @apply text-2xl mb-4; 16 | } 17 | 18 | .category { 19 | @apply flex flex-col gap-4 text-gray; 20 | 21 | label::before { 22 | flex-shrink: 0; 23 | } 24 | } 25 | 26 | .group { 27 | @apply flex gap-4 text-gray; 28 | 29 | label::before { 30 | flex-shrink: 0; 31 | } 32 | } 33 | } 34 | 35 | .submit { 36 | @apply text-3xl bg-primary flex flex-col items-center justify-center rounded-r-xl absolute top-0 right-0 h-full w-1/6; 37 | } 38 | } 39 | 40 | @media (max-width: $adaptive-max-breakpoint) { 41 | .form { 42 | @apply flex-col; 43 | 44 | .inputs { 45 | @apply w-auto; 46 | } 47 | 48 | .buttons { 49 | @apply border-none px-0 mb-32; 50 | } 51 | 52 | .group { 53 | @apply flex-col; 54 | } 55 | 56 | .submit { 57 | @apply bottom-0 left-0 h-32 w-full rounded-none rounded-b-xl top-auto right-auto; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoriesSearch/CategoriesSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | 3 | import styles from "./CategoriesSearch.module.scss"; 4 | import InputPrimary from "@/components/UI/Input/InputPrimary/InputPrimary"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import { useDebounce } from "@/hooks/useDebounce"; 7 | import { categoriesApi } from "@/store/api/categories/categories.api"; 8 | import { CategoryType } from "@/store/api/categories/categories.types"; 9 | 10 | interface Props { 11 | setCategories: React.Dispatch>; 12 | } 13 | 14 | const CategoriesSearch: FC = ({ setCategories }) => { 15 | const [value, setValue] = useState(""); 16 | const debounce = useDebounce(value, 500); 17 | 18 | const { data } = categoriesApi.useGetCategoriesQuery({ search: debounce }); 19 | 20 | useEffect(() => { 21 | data && setCategories(data); 22 | }, [data, setCategories]); 23 | 24 | return ( 25 |
26 | setValue(event.target.value)} 29 | className={styles.input} 30 | title="Название категории" 31 | /> 32 | 35 |
36 | ); 37 | }; 38 | 39 | export default CategoriesSearch; 40 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryDeleteModal/CategoryDeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import Button from "@/components/UI/Button/Button"; 4 | 5 | import styles from "./CategoryDeleteModal.module.scss"; 6 | import { categoriesApi } from "@/store/api/categories/categories.api"; 7 | 8 | interface Props { 9 | modalShown: boolean; 10 | setModalShown: React.Dispatch>; 11 | categoryId: number; 12 | } 13 | 14 | const CategoryDeleteModal: FC = ({ 15 | modalShown, 16 | setModalShown, 17 | categoryId 18 | }) => { 19 | const [deleteCategory] = categoriesApi.useDeleteCategoryMutation(); 20 | 21 | const handleSubmit = () => { 22 | deleteCategory({ categoryId: categoryId }).then(() => setModalShown(false)); 23 | }; 24 | 25 | return ( 26 | 31 |

Вы уверены что хотите удалить категорию?

32 |
33 | 36 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default CategoryDeleteModal; 45 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{ts,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: "#0075FF", 8 | "primary-active": "#2F8FFF", 9 | secondary: "#282828", 10 | "secondary-hover": "#212121", 11 | "secondary-active": "#121212", 12 | gray: "#777777", 13 | "gray-black": "#222222", 14 | black: "#282828", 15 | info: "#E6F3FF", 16 | success: "#E6FFED", 17 | warning: "#FF0000", 18 | notification: "#E6F3FF" 19 | }, 20 | borderRadius: { 21 | sm: "4px", 22 | md: "10px", 23 | lg: "15px", 24 | xl: "24px" 25 | }, 26 | fontFamily: { 27 | montserrat: ["Montserrat", "sans-serif"] 28 | }, 29 | fontSize: { 30 | xs: "12px", 31 | sm: "14px", 32 | base: "15px", 33 | lg: "16px", 34 | xl: "18px", 35 | "2xl": "20px", 36 | "3xl": "25px", 37 | "4xl": "30px", 38 | "5xl": "40px", 39 | "6xl": "48px" 40 | }, 41 | boxShadow: { 42 | md: "0px 0px 60px 2px rgba(0, 0, 0, 0.04)" 43 | } 44 | }, 45 | container: { 46 | center: true, 47 | screens: { 48 | xl: "1200px" 49 | }, 50 | padding: { 51 | DEFAULT: "1rem", 52 | xl: 0 53 | } 54 | } 55 | }, 56 | plugins: [] 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleCard/ScheduleCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./ScheduleCard.module.scss"; 4 | import Button from "@/components/UI/Button/Button"; 5 | import ScheduleModal from "@/components/pages/Profile/Teacher/ScheduleModal/ScheduleModal"; 6 | import { AnimatePresence } from "framer-motion"; 7 | import { GroupType } from "@/store/api/groups/groups.types"; 8 | import { ScheduleType } from "@/store/api/schedules/schedules.types"; 9 | import { UserRole } from "@/store/api/users/users.types"; 10 | 11 | interface Props { 12 | groups: GroupType[]; 13 | schedules: ScheduleType[]; 14 | teacherType: UserRole; 15 | } 16 | 17 | const ScheduleCard: FC = ({ groups, schedules, teacherType }) => { 18 | const [modalShown, setModalShown] = useState(false); 19 | return ( 20 | <> 21 | 27 | 28 | {modalShown && ( 29 | 36 | )} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default ScheduleCard; 43 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentDeleteModal/StudentDeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import Button from "@/components/UI/Button/Button"; 4 | 5 | import styles from "./StudentDeleteModal.module.scss"; 6 | import { groupsApi } from "@/store/api/groups/groups.api"; 7 | 8 | interface Props { 9 | modalShown: boolean; 10 | setModalShown: React.Dispatch>; 11 | userId: number; 12 | groupId: number; 13 | } 14 | 15 | const StudentDeleteModal: FC = ({ 16 | modalShown, 17 | setModalShown, 18 | userId, 19 | groupId 20 | }) => { 21 | const [deleteStudentWithGroup] = 22 | groupsApi.useDeleteStudentFromGroupMutation(); 23 | 24 | const handleSubmit = () => { 25 | deleteStudentWithGroup({ groupId: groupId, userId: userId }).then(() => 26 | setModalShown(false) 27 | ); 28 | }; 29 | 30 | return ( 31 | 36 |

Вы уверены что хотите отчислить этого ученика?

37 |
38 | 41 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default StudentDeleteModal; 50 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorDeleteModal/InstructorDeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import Button from "@/components/UI/Button/Button"; 4 | 5 | import styles from "./InstructorDeleteModal.module.scss"; 6 | import { usersApi } from "@/store/api/users/users.api"; 7 | import { UserRole } from "@/store/api/users/users.types"; 8 | 9 | interface Props { 10 | modalShown: boolean; 11 | setModalShown: React.Dispatch>; 12 | userId: number; 13 | } 14 | 15 | const InstructorDeleteModal: FC = ({ 16 | modalShown, 17 | setModalShown, 18 | userId 19 | }) => { 20 | const [changeUserRole] = usersApi.useChangeUserRoleMutation(); 21 | 22 | const handleSubmit = () => { 23 | changeUserRole({ 24 | userId: userId, 25 | body: { 26 | role: UserRole.STUDENT 27 | } 28 | }).then(() => setModalShown(false)); 29 | }; 30 | 31 | return ( 32 | 37 |

Вы уверены что хотите удалить этого учителя?

38 |
39 | 42 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default InstructorDeleteModal; 51 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModalItem/ScheduleDeleteModal/ScheduleDeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import styles from "./ScheduleDeleteModal.module.scss"; 4 | import Button from "@/components/UI/Button/Button"; 5 | import { schedulesApi } from "@/store/api/schedules/schedules.api"; 6 | import { ScheduleType } from "@/store/api/schedules/schedules.types"; 7 | 8 | interface Props { 9 | modalShown: boolean; 10 | setModalShown: React.Dispatch>; 11 | schedule: ScheduleType; 12 | } 13 | 14 | const ScheduleDeleteModal: FC = ({ 15 | modalShown, 16 | setModalShown, 17 | schedule 18 | }) => { 19 | const [deleteSchedule] = schedulesApi.useDeleteScheduleMutation(); 20 | 21 | const handleSubmit = () => { 22 | deleteSchedule({ 23 | groupId: schedule.group.id, 24 | scheduleId: schedule.id 25 | }).then(() => setModalShown(false)); 26 | }; 27 | 28 | return ( 29 | 34 |

Вы уверены что хотите удалить занятие?

35 |
36 | 39 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default ScheduleDeleteModal; 48 | -------------------------------------------------------------------------------- /src/store/api/categories/categories.api.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/store/api/api"; 2 | import { CategoryType } from "@/store/api/categories/categories.types"; 3 | 4 | export const categoriesApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | getCategories: builder.query({ 7 | query: (args) => ({ 8 | url: `/categories`, 9 | params: { search: args.search } 10 | }), 11 | providesTags: ["Category"] 12 | }), 13 | getCategoryById: builder.query({ 14 | query: (args) => `/categories/${args.categoryId}`, 15 | providesTags: ["Category"] 16 | }), 17 | createCategory: builder.mutation({ 18 | query: (args) => ({ 19 | url: `/categories/create`, 20 | method: "PUT", 21 | body: { value: args.value } 22 | }), 23 | invalidatesTags: ["Category"] 24 | }), 25 | updateCategory: builder.mutation< 26 | CategoryType, 27 | { categoryId: number; value: string } 28 | >({ 29 | query: (args) => ({ 30 | url: `/categories/${args.categoryId}`, 31 | method: "PATCH", 32 | body: { value: args.value } 33 | }), 34 | invalidatesTags: ["Category", "Group"] 35 | }), 36 | deleteCategory: builder.mutation({ 37 | query: (args) => ({ 38 | url: `/categories/${args.categoryId}`, 39 | method: "DELETE" 40 | }), 41 | invalidatesTags: ["Category", "Group"] 42 | }) 43 | }) 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/pages/Home/PrimarySection/PrimarySection.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .section { 4 | .majorArticle { 5 | @apply relative bg-white shadow-md rounded-lg px-10 py-20 overflow-hidden; 6 | 7 | h1 { 8 | @apply text-6xl font-black uppercase w-3/4; 9 | } 10 | 11 | p { 12 | @apply w-2/4; 13 | } 14 | 15 | button { 16 | @apply px-10 py-3 mt-6 gap-2; 17 | } 18 | 19 | img { 20 | @apply absolute -top-10 -right-80 z-10; 21 | 22 | @media (max-width: $adaptive-max-breakpoint) { 23 | @apply hidden; 24 | } 25 | } 26 | 27 | @media (max-width: $adaptive-max-breakpoint) { 28 | @apply py-6 px-6; 29 | 30 | h1 { 31 | @apply w-full text-3xl; 32 | } 33 | 34 | p { 35 | @apply w-full; 36 | } 37 | } 38 | } 39 | 40 | .items { 41 | @apply grid gap-6; 42 | grid-template-columns: repeat(4, 1fr); 43 | 44 | article { 45 | @apply shadow-md; 46 | } 47 | } 48 | 49 | .items { 50 | @media (max-width: $adaptive-max-breakpoint) { 51 | @apply hidden; 52 | } 53 | } 54 | 55 | .swiper { 56 | @media (min-width: $adaptive-max-breakpoint) { 57 | @apply hidden; 58 | } 59 | } 60 | 61 | .items, 62 | .swiper { 63 | @apply mt-6; 64 | article { 65 | @apply bg-white rounded-md flex flex-col items-center text-center gap-2 px-3 py-4; 66 | 67 | h4 { 68 | @apply text-2xl font-bold; 69 | } 70 | } 71 | } 72 | 73 | .swiper { 74 | .slide { 75 | @apply w-72; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/Admin.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | import UserInfoCard from "@/components/pages/Profile/UserInfoCard/UserInfoCard"; 3 | 4 | import styles from "../Profile.module.scss"; 5 | import NavigationCard from "@/components/pages/Profile/Admin/NavigationCard/NavigationCard"; 6 | import StudentsSection from "@/components/pages/Profile/Admin/StudentsSection/StudentsSection"; 7 | import GroupsSection from "@/components/pages/Profile/Admin/GroupsSection/GroupsSection"; 8 | import InstructorsSection from "@/components/pages/Profile/Admin/InstructorsSection/InstructorsSection"; 9 | import CategoriesSection from "@/components/pages/Profile/Admin/CategoriesSection/CategoriesSection"; 10 | import { UserType } from "@/store/api/users/users.types"; 11 | 12 | interface Props { 13 | user: UserType; 14 | } 15 | 16 | const Admin: FC = ({ user }) => { 17 | const [section, setSection] = useState<1 | 2 | 3 | 4>(1); 18 | return ( 19 |
20 |
21 | 27 | 28 |
29 | <> 30 | {section === 1 && } 31 | {section === 2 && } 32 | {section === 3 && } 33 | {section === 4 && } 34 | 35 |
36 | ); 37 | }; 38 | 39 | export default Admin; 40 | -------------------------------------------------------------------------------- /src/components/pages/Home/QuestionsSection/QuestionsSection.data.ts: -------------------------------------------------------------------------------- 1 | export interface Question { 2 | id: number; 3 | title: string; 4 | description: string; 5 | isOpen: boolean; 6 | } 7 | 8 | export const data: Question[] = [ 9 | { 10 | id: 1, 11 | title: "Как получить налоговый вычет", 12 | description: 13 | "Чтобы вернуть часть денег за обучение в автошколе, необходимо иметь постоянную регистрацию и официальное трудоустройство. Подробнее можно уточнить в Федеральной налоговой службе.", 14 | isOpen: false 15 | }, 16 | { 17 | id: 2, 18 | title: "Сколько раз в неделю проходят занятия", 19 | description: 20 | "Учебный график подбирается индивидуально от потребностей ученика.", 21 | isOpen: false 22 | }, 23 | { 24 | id: 3, 25 | title: "Где оформить медицинскую справку?", 26 | description: 27 | "Чтобы оформить справку для получения или замены водительских прав, вам нужно будет пройти обследование врачом-психиатром-наркологом и врачом-психиатром в наркологических и психоневрологических диспансерах города проживания.", 28 | isOpen: false 29 | }, 30 | { 31 | id: 4, 32 | title: "Кому запрещено водить автомобиль?", 33 | description: 34 | "Вождению припятствуют шизофрения, аффективные расстройства, умственная отсталось, эпилепсия, слепота обоих глаз.", 35 | isOpen: false 36 | }, 37 | { 38 | id: 5, 39 | title: "Сколько стоит пересдать экзамен в автошколе? ", 40 | description: 41 | "Стоимость пересдачи экзамена варируется в зависимости от категории. Цена не превышает 4 000 рублей.", 42 | isOpen: false 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /src/components/UI/Radio/Radio.module.scss: -------------------------------------------------------------------------------- 1 | .radio { 2 | @apply cursor-pointer flex items-center gap-4 relative; 3 | 4 | > input { 5 | @apply absolute opacity-0; 6 | 7 | &:checked { 8 | + h6 { 9 | @apply text-black; 10 | } 11 | } 12 | } 13 | 14 | > &::before { 15 | @apply w-8 h-8 rounded-full border border-gray block; 16 | content: ""; 17 | } 18 | 19 | > &::after { 20 | @apply w-4 h-4 rounded-full bg-gray absolute left-2 hidden; 21 | content: ""; 22 | } 23 | 24 | &:has(:checked)::before { 25 | @apply border-gray border-2; 26 | } 27 | 28 | &:has(:focus)::before { 29 | @apply border-gray border-2; 30 | } 31 | 32 | &:has(:checked)::after { 33 | @apply block; 34 | } 35 | 36 | &:has(:focus)::after { 37 | @apply block; 38 | } 39 | 40 | &.dark { 41 | > input { 42 | @apply absolute opacity-0; 43 | 44 | h6 { 45 | @apply text-gray; 46 | } 47 | 48 | &:checked { 49 | + h6 { 50 | @apply text-white; 51 | } 52 | } 53 | } 54 | 55 | > &::before { 56 | @apply w-8 h-8 rounded-full border border-gray block; 57 | content: ""; 58 | } 59 | 60 | > &::after { 61 | @apply w-4 h-4 rounded-full bg-white absolute left-2 hidden; 62 | content: ""; 63 | } 64 | 65 | &:has(:checked)::before { 66 | @apply border-white border-2; 67 | } 68 | 69 | &:has(:focus)::before { 70 | @apply border-white border-2; 71 | } 72 | 73 | &:has(:checked)::after { 74 | @apply block; 75 | } 76 | 77 | &:has(:focus)::after { 78 | @apply block; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorsSearch/InstructorsSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | 3 | import styles from "./InstructorsSearch.module.scss"; 4 | import InputPrimary from "@/components/UI/Input/InputPrimary/InputPrimary"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import { useDebounce } from "@/hooks/useDebounce"; 7 | import { usersApi } from "@/store/api/users/users.api"; 8 | import { UserRole, UserType } from "@/store/api/users/users.types"; 9 | 10 | interface Props { 11 | setInstructors: React.Dispatch>; 12 | } 13 | 14 | const InstructorsSearch: FC = ({ setInstructors }) => { 15 | const [value, setValue] = useState(""); 16 | const debounce = useDebounce(value, 500); 17 | 18 | const theory = usersApi.useGetUsersQuery({ 19 | role: UserRole.THEORY_TEACHER, 20 | search: debounce 21 | }).data; 22 | 23 | const practice = usersApi.useGetUsersQuery({ 24 | role: UserRole.PRACTICE_TEACHER, 25 | search: debounce 26 | }).data; 27 | 28 | useEffect(() => { 29 | theory && practice && setInstructors(theory.concat(practice)); 30 | }, [practice, theory, setInstructors]); 31 | 32 | return ( 33 |
34 | setValue(event.target.value)} 37 | className={styles.input} 38 | title="Имя преподавателя" 39 | /> 40 | 43 |
44 | ); 45 | }; 46 | 47 | export default InstructorsSearch; 48 | -------------------------------------------------------------------------------- /src/components/pages/Profile/UserInfoCard/UserInfoCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./UserInfoCard.module.scss"; 4 | import Avatar from "@/components/other/Icons/Avatar"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import { useTypedDispatch } from "@/hooks/useTypedDispatch"; 7 | import { logout } from "@/store/auth/auth.slice"; 8 | import { UserRole } from "@/store/api/users/users.types"; 9 | 10 | interface Props { 11 | surname: string; 12 | name: string; 13 | role: UserRole; 14 | patronymic?: string; 15 | category?: string; 16 | } 17 | 18 | const UserInfoCard: FC = ({ 19 | surname, 20 | category, 21 | name, 22 | role, 23 | patronymic 24 | }) => { 25 | const dispatch = useTypedDispatch(); 26 | 27 | const handleLogout = () => { 28 | dispatch(logout()); 29 | }; 30 | 31 | return ( 32 | 53 | ); 54 | }; 55 | 56 | export default UserInfoCard; 57 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Student/Student.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import UserInfoCard from "@/components/pages/Profile/UserInfoCard/UserInfoCard"; 3 | import TeachersCard from "@/components/pages/Profile/Student/TeachersCard/TeachersCard"; 4 | import ErrorMessage from "@/components/pages/Profile/ErrorMessage/ErrorMessage"; 5 | 6 | import styles from "../Profile.module.scss"; 7 | import { UserType } from "@/store/api/users/users.types"; 8 | import StudentScheduleItem from "@/components/pages/Profile/Student/StudentScheduleItem/StudentScheduleItem"; 9 | 10 | interface Props { 11 | user: UserType; 12 | } 13 | 14 | const Student: FC = ({ user }) => { 15 | return ( 16 |
17 |
18 | 25 | {user.group && ( 26 | 30 | )} 31 |
32 | {!user.group ? ( 33 | 34 | ) : ( 35 |
36 |
    37 | {user.group.schedules.map((schedule) => ( 38 |
  • 39 | 40 |
  • 41 | ))} 42 |
43 |
44 | )} 45 |
46 | ); 47 | }; 48 | 49 | export default Student; 50 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorCreateModal/InstructorCreateModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import ExitThin from "@/components/other/Icons/ExitThin"; 4 | 5 | import styles from "./InstructorCreateModal.module.scss"; 6 | import InstructorCreateItem from "@/components/pages/Profile/Admin/InstructorsSection/InstructorCreateModal/InstructorCreateItem/InstructorCreateItem"; 7 | import { usersApi } from "@/store/api/users/users.api"; 8 | import { UserRole } from "@/store/api/users/users.types"; 9 | 10 | interface Props { 11 | modalShown: boolean; 12 | setModalShown: React.Dispatch>; 13 | } 14 | 15 | const InstructorCreateModal: FC = ({ modalShown, setModalShown }) => { 16 | const users = usersApi.useGetUsersQuery({ 17 | role: UserRole.STUDENT, 18 | withGroup: false 19 | }).data; 20 | 21 | return ( 22 | 27 |
28 |

Добавить преподавателя {users && `(${users.length})`}

29 | 32 |
33 |
34 | {users && 35 | users.map((user) => ( 36 | 41 | ))} 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default InstructorCreateModal; 48 | -------------------------------------------------------------------------------- /src/components/other/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import HeaderMobile from "./HeaderMobile/HeaderMobile"; 4 | import HeaderDesktop from "./HeaderDesktop/HeaderDesktop"; 5 | 6 | import styles from "./Header.module.scss"; 7 | import { AnimatePresence } from "framer-motion"; 8 | import LoginModal from "../ModalWrapper/LoginModal/LoginModal"; 9 | import RegisterModal from "../ModalWrapper/RegisterModal/RegisterModal"; 10 | import dynamic from "next/dynamic"; 11 | 12 | const ModalWrapper = dynamic(import("../ModalWrapper/ModalWrapper"), { 13 | ssr: false 14 | }); 15 | 16 | const Header: FC = () => { 17 | const [isModalShow, setIsModalShow] = useState(false); 18 | const [modalType, setModalType] = useState<"login" | "register">("login"); 19 | 20 | return ( 21 | 46 | ); 47 | }; 48 | 49 | export default Header; 50 | -------------------------------------------------------------------------------- /src/components/UI/Input/InputPrimary/InputPrimary.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | HTMLInputTypeAttribute, 4 | InputHTMLAttributes 5 | } from "react"; 6 | 7 | import styles from "./InputPrimary.module.scss"; 8 | import InputPrimaryPhone from "./InputPrimaryPhone/InputPrimaryPhone"; 9 | import classNames from "classnames"; 10 | 11 | interface Props extends InputHTMLAttributes { 12 | title: string; 13 | error?: string; 14 | type?: HTMLInputTypeAttribute; 15 | } 16 | 17 | const InputPrimary = forwardRef( 18 | ({ title, error, className, ...props }, ref) => { 19 | return ( 20 |
29 | 52 |

{error}

53 |
54 | ); 55 | } 56 | ); 57 | 58 | InputPrimary.displayName = "InputPrimary"; 59 | 60 | export default InputPrimary; 61 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | import StudentItem from "@/components/pages/Profile/Admin/StudentsSection/StudentItem/StudentItem"; 3 | 4 | import styles from "./StudentsSection.module.scss"; 5 | import StudentSearch from "@/components/pages/Profile/Admin/StudentsSection/StudentSearch/StudentSearch"; 6 | import Button from "@/components/UI/Button/Button"; 7 | import StudentCreateModal from "@/components/pages/Profile/Admin/StudentsSection/StudentCreateModal/StudentCreateModal"; 8 | import { AnimatePresence } from "framer-motion"; 9 | import { UserType } from "@/store/api/users/users.types"; 10 | 11 | const StudentsSection: FC = () => { 12 | const [students, setStudents] = useState([]); 13 | const [modalCreateShown, setModalCreateShown] = useState(false); 14 | 15 | return ( 16 |
17 | 18 |
19 |
20 |

Ученики ({students.length})

21 | 24 |
25 |
    26 | {students.map((user) => ( 27 |
  • 28 | 29 |
  • 30 | ))} 31 |
32 |
33 | 34 | {modalCreateShown && ( 35 | 39 | )} 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default StudentsSection; 46 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/Teacher.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styles from "../Profile.module.scss"; 3 | import UserInfoCard from "@/components/pages/Profile/UserInfoCard/UserInfoCard"; 4 | import ScheduleCard from "@/components/pages/Profile/Teacher/ScheduleCard/ScheduleCard"; 5 | import { UserType } from "@/store/api/users/users.types"; 6 | import { groupsApi } from "@/store/api/groups/groups.api"; 7 | import { schedulesApi } from "@/store/api/schedules/schedules.api"; 8 | import TeacherScheduleItem from "@/components/pages/Profile/Teacher/TeacherScheduleItem/TeacherScheduleItem"; 9 | 10 | interface Props { 11 | user: UserType; 12 | } 13 | 14 | const Teacher: FC = ({ user }) => { 15 | const groups = groupsApi.useGetGroupsQuery({ search: user.email }).data; 16 | const schedules = schedulesApi.useGetSchedulesQuery({ 17 | teacherId: user.id 18 | }).data; 19 | return ( 20 |
21 |
22 | 28 | {groups && schedules && ( 29 | 34 | )} 35 |
36 |
37 |
    38 | {schedules && 39 | schedules.map((schedule) => ( 40 |
  • 41 | 42 |
  • 43 | ))} 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default Teacher; 51 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/NavigationCard/NavigationCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./NavigationCard.module.scss"; 4 | import Arrow from "@/components/other/Icons/Arrow"; 5 | import classNames from "classnames"; 6 | 7 | interface Props { 8 | section: 1 | 2 | 3 | 4; 9 | setSection: React.Dispatch>; 10 | } 11 | 12 | const NavigationCard: FC = ({ section, setSection }) => { 13 | return ( 14 | 61 | ); 62 | }; 63 | 64 | export default NavigationCard; 65 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/ScheduleModal/ScheduleModalItem/ScheduleModalItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./ScheduleModalItem.module.scss"; 4 | import { ScheduleType } from "@/store/api/schedules/schedules.types"; 5 | import Avatar from "@/components/other/Icons/Avatar"; 6 | import Button from "@/components/UI/Button/Button"; 7 | import { getScheduleType } from "@/utils/getScheduleType.utils"; 8 | import { AnimatePresence } from "framer-motion"; 9 | import ScheduleDeleteModal from "@/components/pages/Profile/Teacher/ScheduleModal/ScheduleModalItem/ScheduleDeleteModal/ScheduleDeleteModal"; 10 | 11 | interface Props { 12 | schedule: ScheduleType; 13 | } 14 | 15 | const ScheduleModalItem: FC = ({ schedule }) => { 16 | const [deleteModalShown, setDeleteModalShown] = useState(false); 17 | return ( 18 | <> 19 |
20 |
21 | 22 |

{schedule.date.replaceAll("-", ".")}

23 |
24 |

{getScheduleType(schedule.type)}

25 |
26 |

27 | с {schedule.startTime} до {schedule.endTime} 28 |

29 | 32 |
33 |
34 | 35 | {deleteModalShown && ( 36 | 41 | )} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default ScheduleModalItem; 48 | -------------------------------------------------------------------------------- /src/store/api/schedules/schedules.api.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/store/api/api"; 2 | import { 3 | CreateScheduleType, 4 | ScheduleType, 5 | UpdateScheduleType 6 | } from "@/store/api/schedules/schedules.types"; 7 | 8 | export const schedulesApi = api.injectEndpoints({ 9 | endpoints: (builder) => ({ 10 | getSchedules: builder.query({ 11 | query: (args) => ({ 12 | url: `/schedules`, 13 | params: { teacherId: args.teacherId } 14 | }), 15 | providesTags: ["Schedule"] 16 | }), 17 | getSchedule: builder.query({ 18 | query: (args) => `/schedules/${args.groupId}`, 19 | providesTags: ["Schedule"] 20 | }), 21 | createSchedule: builder.mutation< 22 | ScheduleType, 23 | { groupId: number | "DEFAULT"; body: CreateScheduleType } 24 | >({ 25 | query: (args) => ({ 26 | url: `/schedules/${args.groupId}/create`, 27 | method: "PUT", 28 | body: { ...args.body } 29 | }), 30 | invalidatesTags: ["Schedule", "Group"] 31 | }), 32 | updateSchedule: builder.mutation< 33 | ScheduleType, 34 | { groupId: number; scheduleId: number; body: UpdateScheduleType } 35 | >({ 36 | query: (args) => ({ 37 | url: `/schedules/${args.groupId}/${args.scheduleId}`, 38 | method: "PATCH", 39 | body: { ...args.body } 40 | }), 41 | invalidatesTags: ["Schedule", "Group"] 42 | }), 43 | deleteSchedule: builder.mutation< 44 | ScheduleType, 45 | { groupId: number; scheduleId: number } 46 | >({ 47 | query: (args) => ({ 48 | url: `/schedules/${args.groupId}/${args.scheduleId}`, 49 | method: "DELETE" 50 | }), 51 | invalidatesTags: ["Schedule", "Group"] 52 | }) 53 | }) 54 | }); 55 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentCreateModal/StudentCreateModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 3 | import ExitThin from "@/components/other/Icons/ExitThin"; 4 | 5 | import styles from "./StudentCreateModal.module.scss"; 6 | import StudentCreateItem from "@/components/pages/Profile/Admin/StudentsSection/StudentCreateModal/StudentCreateItem/StudentCreateItem"; 7 | import { usersApi } from "@/store/api/users/users.api"; 8 | import { groupsApi } from "@/store/api/groups/groups.api"; 9 | import { UserRole } from "@/store/api/users/users.types"; 10 | 11 | interface Props { 12 | modalShown: boolean; 13 | setModalShown: React.Dispatch>; 14 | } 15 | 16 | const StudentCreateModal: FC = ({ modalShown, setModalShown }) => { 17 | const users = usersApi.useGetUsersQuery({ 18 | role: UserRole.STUDENT, 19 | withGroup: false 20 | }).data; 21 | const groups = groupsApi.useGetGroupsQuery({}).data; 22 | 23 | return ( 24 | 29 |
30 |

Добавить ученика {users && `(${users.length})`}

31 | 34 |
35 |
36 | {users && 37 | groups && 38 | users.map((user) => ( 39 | 45 | ))} 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default StudentCreateModal; 52 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./InstructorsSection.module.scss"; 4 | import InstructorItem from "@/components/pages/Profile/Admin/InstructorsSection/InstructorItem/InstructorItem"; 5 | import InstructorsSearch from "@/components/pages/Profile/Admin/InstructorsSection/InstructorsSearch/InstructorsSearch"; 6 | import Button from "@/components/UI/Button/Button"; 7 | import { AnimatePresence } from "framer-motion"; 8 | import InstructorCreateModal from "@/components/pages/Profile/Admin/InstructorsSection/InstructorCreateModal/InstructorCreateModal"; 9 | import { UserType } from "@/store/api/users/users.types"; 10 | 11 | const InstructorsSection: FC = () => { 12 | const [modalCreateShown, setModalCreateShown] = useState(false); 13 | const [instructors, setInstructors] = useState([]); 14 | 15 | return ( 16 |
17 | 18 |
19 |
20 |

Преподаватели ({instructors.length})

21 | 24 |
25 |
    26 | {instructors.map((teacher) => ( 27 |
  • 28 | 29 |
  • 30 | ))} 31 |
32 |
33 | 34 | {modalCreateShown && ( 35 | 39 | )} 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default InstructorsSection; 46 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoriesSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./CategoriesSection.module.scss"; 4 | import Button from "@/components/UI/Button/Button"; 5 | import { AnimatePresence } from "framer-motion"; 6 | import CategoriesSearch from "@/components/pages/Profile/Admin/CategoriesSection/CategoriesSearch/CategoriesSearch"; 7 | import CategoryCreateModal from "@/components/pages/Profile/Admin/CategoriesSection/CategoryCreateModal/CategoryCreateModal"; 8 | import CategoryItem from "@/components/pages/Profile/Admin/CategoriesSection/CategoryItem/CategoryItem"; 9 | import { CategoryType } from "@/store/api/categories/categories.types"; 10 | 11 | const CategoriesSection: FC = () => { 12 | const [categories, setCategories] = useState([]); 13 | const [modalCreateShown, setModalCreateShown] = useState(false); 14 | 15 | return ( 16 |
17 | 18 |
19 |
20 |

Категории ({categories.length})

21 | 24 |
25 |
    26 | {categories && 27 | categories.map((category) => ( 28 |
  • 29 | 30 |
  • 31 | ))} 32 |
33 |
34 | 35 | {modalCreateShown && ( 36 | 40 | )} 41 | 42 |
43 | ); 44 | }; 45 | 46 | export default CategoriesSection; 47 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextProgressBar from "nextjs-progressbar"; 3 | import { NextPage } from "next"; 4 | import { AppProps } from "next/app"; 5 | 6 | import "../styles/globals.scss"; 7 | import Head from "next/head"; 8 | import { Provider } from "react-redux"; 9 | import { persistor, store } from "@/store"; 10 | import { PersistGate } from "redux-persist/integration/react"; 11 | import AuthProvider from "@/providers/AuthProvider"; 12 | import { TypeComponentAuthField } from "@/providers/PrivateRouter.types"; 13 | 14 | type TypeAppProps = AppProps & TypeComponentAuthField; 15 | 16 | const App: NextPage = ({ Component, pageProps }) => { 17 | return ( 18 | <> 19 | 20 | 21 | 22 | 28 | 29 | 33 | 37 | 38 | 39 | Driving School 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryItem/CategoryItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./CategoryItem.module.scss"; 4 | import Button from "@/components/UI/Button/Button"; 5 | import { AnimatePresence } from "framer-motion"; 6 | import CategoryDeleteModal from "@/components/pages/Profile/Admin/CategoriesSection/CategoryDeleteModal/CategoryDeleteModal"; 7 | import CategoryChangeModal from "@/components/pages/Profile/Admin/CategoriesSection/CategoryChangeModal/CategoryChangeModal"; 8 | import { CategoryType } from "@/store/api/categories/categories.types"; 9 | 10 | interface Props { 11 | category: CategoryType; 12 | } 13 | 14 | const CategoryItem: FC = ({ category }) => { 15 | const [modalDeleteShown, setModalDeleteShown] = useState(false); 16 | const [modalChangeShown, setModalChangeShown] = useState(false); 17 | 18 | return ( 19 |
20 |

Категория {category.value}

21 |
22 | 25 | 28 |
29 | 30 | {modalDeleteShown && ( 31 | 36 | )} 37 | {modalChangeShown && ( 38 | 43 | )} 44 | 45 |
46 | ); 47 | }; 48 | 49 | export default CategoryItem; 50 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryCreateModal/CategoryCreateModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent, useState } from "react"; 2 | import ExitThin from "@/components/other/Icons/ExitThin"; 3 | import Button from "@/components/UI/Button/Button"; 4 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 5 | 6 | import styles from "./CategoryCreateModal.module.scss"; 7 | import InputPrimary from "@/components/UI/Input/InputPrimary/InputPrimary"; 8 | import { categoriesApi } from "@/store/api/categories/categories.api"; 9 | 10 | interface Props { 11 | modalShown: boolean; 12 | setModalShown: React.Dispatch>; 13 | } 14 | 15 | const CategoryCreateModal: FC = ({ modalShown, setModalShown }) => { 16 | const [value, setValue] = useState(""); 17 | const [createCategory] = categoriesApi.useCreateCategoryMutation(); 18 | 19 | const handleSubmit = (event: FormEvent) => { 20 | event.preventDefault(); 21 | value && createCategory({ value: value }).then(() => setModalShown(false)); 22 | }; 23 | 24 | return ( 25 | 30 |
31 |

Создать категорию

32 | 35 |
36 |
37 |
handleSubmit(event)}> 38 | setValue(event.target.value)} 40 | title="Название новой категории" 41 | /> 42 | 45 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default CategoryCreateModal; 53 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentCreateModal/StudentCreateItem/StudentCreateItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./StudentCreateItem.module.scss"; 4 | import Avatar from "@/components/other/Icons/Avatar"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import Select, { SelectTypes } from "@/components/UI/Select/Select"; 7 | import { GroupType } from "@/store/api/groups/groups.types"; 8 | import { UserType } from "@/store/api/users/users.types"; 9 | import { groupsApi } from "@/store/api/groups/groups.api"; 10 | 11 | interface Props { 12 | setModalShown: React.Dispatch>; 13 | user: UserType; 14 | groups: GroupType[]; 15 | } 16 | 17 | const StudentCreateItem: FC = ({ setModalShown, groups, user }) => { 18 | const [selectedGroup, setSelectedGroup] = useState( 19 | groups.length ? groups[0].id : 0 20 | ); 21 | 22 | const [addStudentToGroup] = groupsApi.usePushStudentToGroupMutation(); 23 | 24 | const handleSubmit = () => { 25 | addStudentToGroup({ groupId: selectedGroup, userId: user.id }).then(() => 26 | setModalShown(false) 27 | ); 28 | }; 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 |

36 | {user.surname} {user.name[0]}.{" "} 37 | {user.patronymic && user.patronymic[0] + "."} 38 |

39 |
40 |
41 |
42 | 48 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default StudentCreateItem; 57 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/CategoriesSection/CategoryChangeModal/CategoryChangeModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent, useState } from "react"; 2 | import ExitThin from "@/components/other/Icons/ExitThin"; 3 | import Button from "@/components/UI/Button/Button"; 4 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 5 | 6 | import styles from "./CategoryChangeModal.module.scss"; 7 | import InputPrimary from "@/components/UI/Input/InputPrimary/InputPrimary"; 8 | import { categoriesApi } from "@/store/api/categories/categories.api"; 9 | 10 | interface Props { 11 | modalShown: boolean; 12 | setModalShown: React.Dispatch>; 13 | categoryId: number; 14 | } 15 | 16 | const CategoryChangeModal: FC = ({ 17 | categoryId, 18 | modalShown, 19 | setModalShown 20 | }) => { 21 | const [value, setValue] = useState(""); 22 | const [changeCategory] = categoriesApi.useUpdateCategoryMutation(); 23 | 24 | const handleSubmit = (event: FormEvent) => { 25 | event.preventDefault(); 26 | value && 27 | changeCategory({ categoryId: categoryId, value: value }).then(() => 28 | setModalShown(false) 29 | ); 30 | }; 31 | 32 | return ( 33 | 38 |
39 |

Редактирование категории

40 | 43 |
44 |
45 |
handleSubmit(event)}> 46 | setValue(event.target.value)} 48 | title="Новое название категории" 49 | /> 50 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default CategoryChangeModal; 60 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Teacher/TeacherScheduleItem/TeacherScheduleItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import { motion } from "framer-motion"; 4 | import styles from "./TeacherScheduleItem.module.scss"; 5 | import { ScheduleType } from "@/store/api/schedules/schedules.types"; 6 | import { AnimatePresence } from "framer-motion"; 7 | import classNames from "classnames"; 8 | import ListArrow from "@/components/other/Icons/ListArrow"; 9 | import Location from "@/components/other/Icons/Location"; 10 | 11 | interface Props { 12 | schedule: ScheduleType; 13 | } 14 | 15 | const TeacherScheduleItem: FC = ({ schedule }) => { 16 | const [isShow, setIsShow] = useState(false); 17 | return ( 18 |
19 |
20 |
21 |

Группа №{schedule.group.id}

22 |

23 | {schedule.date} - с {schedule.startTime} до {schedule.endTime} 24 |

25 |
26 | {schedule.address && ( 27 | 35 | )} 36 |
37 | 38 | {isShow && ( 39 | 45 | {schedule.address && ( 46 |

47 | 48 | Адрес:{" "} 49 | 50 | {schedule.address} 51 |

52 | )} 53 |
54 | )} 55 |
56 |
57 | ); 58 | }; 59 | 60 | export default TeacherScheduleItem; 61 | -------------------------------------------------------------------------------- /src/components/pages/Home/CategoriesSection/CategoriesSection.module.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/variables"; 2 | 3 | .section { 4 | .categories, 5 | .swiper { 6 | @apply grid gap-5 mt-10; 7 | grid-template-columns: repeat(2, 1fr); 8 | 9 | article { 10 | @apply bg-white flex flex-col justify-between gap-3 rounded-md p-4; 11 | 12 | h3 { 13 | @apply text-3xl font-black; 14 | 15 | > span { 16 | @apply text-primary; 17 | } 18 | } 19 | 20 | > .img { 21 | @apply self-center justify-center h-40; 22 | 23 | > img { 24 | @apply h-full w-full; 25 | } 26 | } 27 | 28 | ul { 29 | @apply flex gap-6; 30 | 31 | > li { 32 | @apply flex items-center gap-2; 33 | 34 | .heading { 35 | @apply font-bold text-2xl; 36 | } 37 | } 38 | } 39 | 40 | button { 41 | @apply py-2 gap-2 w-full; 42 | } 43 | } 44 | } 45 | 46 | .categories { 47 | article { 48 | @apply shadow-md; 49 | ul { 50 | @apply flex-wrap justify-center; 51 | 52 | > li { 53 | .heading { 54 | @apply font-bold text-2xl; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | .swiper { 62 | .slide { 63 | @apply w-72; 64 | } 65 | 66 | article { 67 | > .img { 68 | @apply h-24; 69 | } 70 | 71 | ul { 72 | @apply flex-col; 73 | 74 | img { 75 | @apply shrink-0; 76 | } 77 | 78 | > li { 79 | @apply text-base; 80 | 81 | .heading { 82 | @apply text-lg; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | .categories { 90 | @media (max-width: $adaptive-max-breakpoint) { 91 | @apply hidden; 92 | } 93 | } 94 | 95 | .swiper { 96 | @media (min-width: $adaptive-min-breakpoint) { 97 | @apply hidden; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Student/StudentScheduleItem/StudentScheduleItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import { motion } from "framer-motion"; 4 | import styles from "./StudentScheduleItem.module.scss"; 5 | import { ScheduleType } from "@/store/api/schedules/schedules.types"; 6 | import { getScheduleType } from "@/utils/getScheduleType.utils"; 7 | import { AnimatePresence } from "framer-motion"; 8 | import classNames from "classnames"; 9 | import ListArrow from "@/components/other/Icons/ListArrow"; 10 | import Location from "@/components/other/Icons/Location"; 11 | 12 | interface Props { 13 | schedule: ScheduleType; 14 | } 15 | 16 | const StudentScheduleItem: FC = ({ schedule }) => { 17 | const [isShow, setIsShow] = useState(false); 18 | return ( 19 |
20 |
21 |
22 |

{getScheduleType(schedule.type)}

23 |

24 | {schedule.date} - с {schedule.startTime} до {schedule.endTime} 25 |

26 |
27 | {schedule.address && ( 28 | 36 | )} 37 |
38 | 39 | {isShow && ( 40 | 46 | {schedule.address && ( 47 |

48 | 49 | Адрес:{" "} 50 | 51 | {schedule.address} 52 |

53 | )} 54 |
55 | )} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default StudentScheduleItem; 62 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorCreateModal/InstructorCreateItem/InstructorCreateItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./InstructorCreateItem.module.scss"; 4 | import Avatar from "@/components/other/Icons/Avatar"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import Radio from "@/components/UI/Radio/Radio"; 7 | import { usersApi } from "@/store/api/users/users.api"; 8 | import { UserRole, UserType } from "@/store/api/users/users.types"; 9 | 10 | interface Props { 11 | setModalShown: React.Dispatch>; 12 | user: UserType; 13 | } 14 | 15 | const InstructorCreateItem: FC = ({ setModalShown, user }) => { 16 | const [role, setRole] = useState(""); 17 | const [changeUserRole] = usersApi.useChangeUserRoleMutation(); 18 | 19 | const handleSubmit = () => { 20 | console.log(role); 21 | role && 22 | changeUserRole({ 23 | userId: user.id, 24 | body: { 25 | role: role 26 | } 27 | }).then(() => setModalShown(false)); 28 | }; 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 |

36 | {user.surname} {user.name[0]}.{" "} 37 | {user.patronymic && user.patronymic[0] + "."} 38 |

39 |
40 |
41 |
42 | setRole(event.target.value)} 47 | /> 48 | setRole(event.target.value)} 53 | /> 54 |
55 | 58 |
59 | ); 60 | }; 61 | 62 | export default InstructorCreateItem; 63 | -------------------------------------------------------------------------------- /src/store/api/groups/groups.api.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/store/api/api"; 2 | import { CategoryType } from "@/store/api/categories/categories.types"; 3 | import { 4 | ChangeGroupType, 5 | CreateGroupType, 6 | GroupType 7 | } from "@/store/api/groups/groups.types"; 8 | 9 | export const groupsApi = api.injectEndpoints({ 10 | endpoints: (builder) => ({ 11 | getGroups: builder.query({ 12 | query: (args) => ({ 13 | url: `/groups`, 14 | params: { search: args.search } 15 | }), 16 | providesTags: ["Group"] 17 | }), 18 | getGroupById: builder.query({ 19 | query: (args) => `/groups/${args.groupId}`, 20 | providesTags: ["Group"] 21 | }), 22 | createGroup: builder.mutation({ 23 | query: (args) => ({ 24 | url: `/groups/create`, 25 | method: "PUT", 26 | body: { ...args } 27 | }), 28 | invalidatesTags: ["Group"] 29 | }), 30 | updateGroup: builder.mutation< 31 | CategoryType, 32 | { groupId: number; body: ChangeGroupType } 33 | >({ 34 | query: (args) => ({ 35 | url: `/groups/${args.groupId}`, 36 | method: "PATCH", 37 | body: { ...args.body } 38 | }), 39 | invalidatesTags: ["Group"] 40 | }), 41 | deleteGroup: builder.mutation({ 42 | query: (args) => ({ 43 | url: `/groups/${args.groupId}`, 44 | method: "DELETE" 45 | }), 46 | invalidatesTags: ["Group"] 47 | }), 48 | pushStudentToGroup: builder.mutation< 49 | GroupType, 50 | { groupId: number; userId: number } 51 | >({ 52 | query: (args) => ({ 53 | url: `/groups/${args.groupId}/${args.userId}`, 54 | method: "PATCH" 55 | }), 56 | invalidatesTags: ["User", "Group"] 57 | }), 58 | deleteStudentFromGroup: builder.mutation< 59 | GroupType, 60 | { groupId: number; userId: number } 61 | >({ 62 | query: (args) => ({ 63 | url: `/groups/${args.groupId}/${args.userId}`, 64 | method: "DELETE" 65 | }), 66 | invalidatesTags: ["User", "Group"] 67 | }) 68 | }) 69 | }); 70 | -------------------------------------------------------------------------------- /src/components/pages/Home/QuestionsSection/QuestionsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./QuestionsSection.module.scss"; 4 | import { AnimatePresence, motion } from "framer-motion"; 5 | import classNames from "classnames"; 6 | import { data, Question } from "./QuestionsSection.data"; 7 | import Add from "../../../other/Icons/Add"; 8 | import Heading from "../../../UI/Heading/Heading"; 9 | 10 | const QuestionsSection: FC = () => { 11 | const [questions, setQuestions] = useState(data); 12 | 13 | const handleOpen = (id: number) => { 14 | const current = [...questions]; 15 | current.forEach((question) => { 16 | if (question.id === id) question.isOpen = !question.isOpen; 17 | }); 18 | setQuestions(current); 19 | }; 20 | 21 | return ( 22 | 28 | Ответы на вопросы 29 |
30 | {questions.map((question) => ( 31 |
32 |
handleOpen(question.id)} 34 | className={styles.heading} 35 | > 36 |

{question.title}

37 | 45 |
46 | 47 | {question.isOpen && ( 48 | 54 | {question.description} 55 | 56 | )} 57 | 58 |
59 | ))} 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default QuestionsSection; 66 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorItem/InstructorItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import Button from "@/components/UI/Button/Button"; 4 | 5 | import styles from "./InstructorItem.module.scss"; 6 | import Avatar from "@/components/other/Icons/Avatar"; 7 | import { AnimatePresence } from "framer-motion"; 8 | import InstructorDeleteModal from "@/components/pages/Profile/Admin/InstructorsSection/InstructorDeleteModal/InstructorDeleteModal"; 9 | import { UserRole, UserType } from "@/store/api/users/users.types"; 10 | import InstructorChangeModal 11 | from "@/components/pages/Profile/Admin/InstructorsSection/InstructorChangeModal/InstructorChangeModal"; 12 | 13 | interface Props { 14 | teacher: UserType; 15 | } 16 | 17 | const GroupItem: FC = ({ teacher }) => { 18 | const [modalDeleteShown, setModalDeleteShown] = useState(false); 19 | const [modalChangeShown, setModalChangeShown] = useState(false); 20 | 21 | return ( 22 |
23 |
24 | 25 |

26 | {teacher.surname} {teacher.name[0]}.{" "} 27 | {teacher.patronymic && teacher.patronymic[0] + "."} 28 |

29 |
30 |

31 | {teacher.role === UserRole.THEORY_TEACHER 32 | ? "Учитель теории" 33 | : "Учитель практики"} 34 |

35 |
36 | 37 | 40 |
41 | 42 | {modalDeleteShown && ( 43 | 48 | )} 49 | {modalChangeShown && ( 50 | 55 | )} 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default GroupItem; 62 | -------------------------------------------------------------------------------- /src/components/UI/Input/InputPrimary/InputPrimaryPhone/InputPrimaryPhone.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, InputHTMLAttributes } from "react"; 2 | 3 | interface Props extends InputHTMLAttributes {} 4 | 5 | const InputPrimaryPhone = forwardRef( 6 | ({ ...props }, ref) => { 7 | const PATTERN = /\D/g; 8 | 9 | const getInputNumbersValue = (value: string) => { 10 | return value.replace(PATTERN, ""); 11 | }; 12 | 13 | const handlePhoneInput = (event: React.ChangeEvent) => { 14 | const input = event.target; 15 | let inputNumbersValue = getInputNumbersValue(input.value); 16 | let formattedInputValue = ""; 17 | const selectionStart = input.selectionStart; 18 | 19 | if (!inputNumbersValue) { 20 | return (input.value = ""); 21 | } 22 | 23 | if (input.value.length !== selectionStart) { 24 | return; 25 | } 26 | 27 | console.log(inputNumbersValue); 28 | 29 | if (inputNumbersValue[0] === "9") { 30 | inputNumbersValue = "7" + inputNumbersValue; 31 | } 32 | 33 | formattedInputValue = "+7 "; 34 | 35 | if (inputNumbersValue.length > 1) { 36 | formattedInputValue += "(" + inputNumbersValue.substring(1, 4); 37 | } 38 | if (inputNumbersValue.length >= 5) { 39 | formattedInputValue += ") " + inputNumbersValue.substring(4, 7); 40 | } 41 | if (inputNumbersValue.length >= 8) { 42 | formattedInputValue += "-" + inputNumbersValue.substring(7, 9); 43 | } 44 | if (inputNumbersValue.length >= 10) { 45 | formattedInputValue += "-" + inputNumbersValue.substring(9, 11); 46 | } 47 | 48 | input.value = formattedInputValue; 49 | }; 50 | 51 | const handlePhoneKeyDown = ( 52 | event: React.KeyboardEvent 53 | ) => { 54 | const input = event.target as HTMLInputElement; 55 | if ( 56 | event.key === "Backspace" && 57 | getInputNumbersValue(input.value).length === 1 58 | ) { 59 | input.value = ""; 60 | } 61 | 62 | return input; 63 | }; 64 | 65 | return ( 66 | 72 | ); 73 | } 74 | ); 75 | 76 | InputPrimaryPhone.displayName = "InputPrimaryPhone"; 77 | 78 | export default InputPrimaryPhone; 79 | -------------------------------------------------------------------------------- /src/components/UI/Input/InputSecondary/InputSecondaryPhone/InputSecondaryPhone.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, InputHTMLAttributes } from "react"; 2 | 3 | interface Props extends InputHTMLAttributes {} 4 | 5 | const InputPrimaryPhone = forwardRef( 6 | ({ ...props }, ref) => { 7 | const PATTERN = /\D/g; 8 | 9 | const getInputNumbersValue = (value: string) => { 10 | return value.replace(PATTERN, ""); 11 | }; 12 | 13 | const handlePhoneInput = (event: React.ChangeEvent) => { 14 | const input = event.target; 15 | let inputNumbersValue = getInputNumbersValue(input.value); 16 | let formattedInputValue = ""; 17 | const selectionStart = input.selectionStart; 18 | 19 | if (!inputNumbersValue) { 20 | return (input.value = ""); 21 | } 22 | 23 | if (input.value.length !== selectionStart) { 24 | return; 25 | } 26 | 27 | console.log(inputNumbersValue); 28 | 29 | if (inputNumbersValue[0] === "9") { 30 | inputNumbersValue = "7" + inputNumbersValue; 31 | } 32 | 33 | formattedInputValue = "+7 "; 34 | 35 | if (inputNumbersValue.length > 1) { 36 | formattedInputValue += "(" + inputNumbersValue.substring(1, 4); 37 | } 38 | if (inputNumbersValue.length >= 5) { 39 | formattedInputValue += ") " + inputNumbersValue.substring(4, 7); 40 | } 41 | if (inputNumbersValue.length >= 8) { 42 | formattedInputValue += "-" + inputNumbersValue.substring(7, 9); 43 | } 44 | if (inputNumbersValue.length >= 10) { 45 | formattedInputValue += "-" + inputNumbersValue.substring(9, 11); 46 | } 47 | 48 | input.value = formattedInputValue; 49 | }; 50 | 51 | const handlePhoneKeyDown = ( 52 | event: React.KeyboardEvent 53 | ) => { 54 | const input = event.target as HTMLInputElement; 55 | if ( 56 | event.key === "Backspace" && 57 | getInputNumbersValue(input.value).length === 1 58 | ) { 59 | input.value = ""; 60 | } 61 | 62 | return input; 63 | }; 64 | 65 | return ( 66 | 72 | ); 73 | } 74 | ); 75 | 76 | InputPrimaryPhone.displayName = "InputPrimaryPhone"; 77 | 78 | export default InputPrimaryPhone; 79 | -------------------------------------------------------------------------------- /src/components/other/Icons/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const Avatar: FC = () => { 4 | return ( 5 | 12 | 13 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default Avatar; 36 | -------------------------------------------------------------------------------- /src/components/other/Header/HeaderDesktop/HeaderDesktop.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./HeaderDesktop.module.scss"; 4 | 5 | import Logo from "../../../UI/Logo/Logo"; 6 | import Link from "next/link"; 7 | import Button from "../../../UI/Button/Button"; 8 | import { useAuth } from "@/hooks/useAuth"; 9 | import { useRouter } from "next/router"; 10 | import { useTypedDispatch } from "@/hooks/useTypedDispatch"; 11 | import { logout } from "@/store/auth/auth.slice"; 12 | 13 | interface Props { 14 | setIsModalShow: React.Dispatch>; 15 | } 16 | 17 | const HeaderDesktop: FC = ({ setIsModalShow }) => { 18 | const dispatch = useTypedDispatch(); 19 | const { pathname } = useRouter(); 20 | 21 | const handleLogout = () => { 22 | dispatch(logout()); 23 | }; 24 | 25 | return ( 26 |
27 |
28 | 29 | 46 |
47 |
48 | 55 | {useAuth() ? ( 56 | pathname.includes("profile") ? ( 57 | 60 | ) : ( 61 | 62 | 65 | 66 | ) 67 | ) : ( 68 | 75 | )} 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default HeaderDesktop; 82 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./GroupsSection.module.scss"; 4 | import GroupItem from "@/components/pages/Profile/Admin/GroupsSection/GroupItem/GroupItem"; 5 | import GroupSearch from "@/components/pages/Profile/Admin/GroupsSection/GroupSearch/GroupSearch"; 6 | import Button from "@/components/UI/Button/Button"; 7 | import { AnimatePresence } from "framer-motion"; 8 | import GroupCreateModal from "@/components/pages/Profile/Admin/GroupsSection/GroupCreateModal/GroupCreateModal"; 9 | import { GroupType } from "@/store/api/groups/groups.types"; 10 | import { categoriesApi } from "@/store/api/categories/categories.api"; 11 | import { UserRole } from "@/store/api/users/users.types"; 12 | import { usersApi } from "@/store/api/users/users.api"; 13 | 14 | const GroupsSection: FC = () => { 15 | const [groups, setGroups] = useState([]); 16 | const [modalCreateShown, setModalCreateShown] = useState(false); 17 | 18 | const theoryTeachers = usersApi.useGetUsersQuery({ 19 | role: UserRole.THEORY_TEACHER 20 | }).data; 21 | 22 | const practiceTeachers = usersApi.useGetUsersQuery({ 23 | role: UserRole.PRACTICE_TEACHER 24 | }).data; 25 | 26 | const categories = categoriesApi.useGetCategoriesQuery({}).data; 27 | 28 | return ( 29 |
30 | 31 |
32 |
33 |

Группы ({groups.length})

34 | 37 |
38 |
    39 | {theoryTeachers && practiceTeachers && categories && groups.map((group) => ( 40 |
  • 41 | 42 |
  • 43 | ))} 44 |
45 |
46 | 47 | {modalCreateShown && 48 | categories && 49 | theoryTeachers && 50 | practiceTeachers && ( 51 | 58 | )} 59 | 60 |
61 | ); 62 | }; 63 | 64 | export default GroupsSection; 65 | -------------------------------------------------------------------------------- /src/components/other/ModalWrapper/ModalWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, 3 | HTMLAttributes, 4 | PropsWithChildren, 5 | useEffect, 6 | useRef 7 | } from "react"; 8 | 9 | import styles from "./ModalWrapper.module.scss"; 10 | import { createPortal } from "react-dom"; 11 | import classNames from "classnames"; 12 | import { motion } from "framer-motion"; 13 | import { createFocusTrap } from "focus-trap"; 14 | 15 | interface Props extends HTMLAttributes { 16 | isShow: boolean; 17 | setIsShow: React.Dispatch>; 18 | } 19 | 20 | const ModalWrapper: FC> = ({ 21 | isShow, 22 | setIsShow, 23 | children, 24 | className, 25 | ...props 26 | }) => { 27 | const ref = useRef(null); 28 | 29 | useEffect(() => { 30 | const trap = createFocusTrap(ref.current as HTMLDivElement, { 31 | allowOutsideClick: true 32 | }); 33 | 34 | if (isShow) trap.activate(); 35 | 36 | return () => { 37 | trap.deactivate(); 38 | }; 39 | }, [isShow]); 40 | 41 | useEffect(() => { 42 | if (isShow) document.documentElement.classList.add("--prevent-scroll"); 43 | 44 | return () => { 45 | document.documentElement.classList.remove("--prevent-scroll"); 46 | }; 47 | }, [isShow]); 48 | 49 | useEffect(() => { 50 | const documentKeydownListener = (event: KeyboardEvent) => { 51 | if (event.key === "Escape") setIsShow(false); 52 | }; 53 | 54 | document.addEventListener("keydown", documentKeydownListener); 55 | 56 | return () => { 57 | document.removeEventListener("keydown", documentKeydownListener); 58 | }; 59 | }, [setIsShow]); 60 | 61 | return createPortal( 62 | setIsShow(false)} 67 | className={styles.modal} 68 | > 69 | 74 |
event.stopPropagation()} 76 | onKeyDown={(event) => event.stopPropagation()} 77 | className={classNames( 78 | { 79 | [styles.inner]: true 80 | }, 81 | className 82 | )} 83 | ref={ref} 84 | {...props} 85 | > 86 | {children} 87 |
88 |
89 |
, 90 | document.getElementById("overlay") as HTMLElement 91 | ); 92 | }; 93 | 94 | export default ModalWrapper; 95 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/StudentsSection/StudentItem/StudentItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./StudentItem.module.scss"; 4 | import Avatar from "@/components/other/Icons/Avatar"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import { AnimatePresence } from "framer-motion"; 7 | import StudentDeleteModal from "@/components/pages/Profile/Admin/StudentsSection/StudentDeleteModal/StudentDeleteModal"; 8 | import { UserType } from "@/store/api/users/users.types"; 9 | 10 | interface Props { 11 | user: UserType; 12 | groupId?: number; 13 | } 14 | 15 | const StudentItem: FC = ({ user, groupId }) => { 16 | const [modalDeleteShown, setModalDeleteShown] = useState(false); 17 | 18 | return ( 19 |
20 |
21 | 22 |
23 |

24 | {user.surname} {user.name[0]}.{" "} 25 | {user.patronymic && user.patronymic[0] + "."} 26 |

27 | {user.group && ( 28 |

29 | Категория: {user.group.category.value} 30 |

31 | )} 32 |
33 |
34 | {user.group && ( 35 |
36 |
37 |

Учитель теории

38 |

39 | {user.group.theoryTeacher.surname}{" "} 40 | {user.group.theoryTeacher.name[0]}.{" "} 41 | {user.group.theoryTeacher.patronymic && 42 | user.group.theoryTeacher.patronymic[0] + "."} 43 |

44 |
45 |
46 |

Учитель практики

47 |

48 | {user.group.practiceTeacher.surname}{" "} 49 | {user.group.practiceTeacher.name[0]}.{" "} 50 | {user.group.practiceTeacher.patronymic && 51 | user.group.practiceTeacher.patronymic[0] + "."} 52 |

53 |
54 |
55 | )} 56 | 59 | 60 | {modalDeleteShown && ( 61 | 67 | )} 68 | 69 |
70 | ); 71 | }; 72 | 73 | export default StudentItem; 74 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/InstructorsSection/InstructorChangeModal/InstructorChangeModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC, FormEvent, useState} from "react"; 2 | import ExitThin from "@/components/other/Icons/ExitThin"; 3 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 4 | 5 | import styles from "./InstructorChangeModal.module.scss"; 6 | import {UserRole, UserType} from "@/store/api/users/users.types"; 7 | import Button from "@/components/UI/Button/Button"; 8 | import Avatar from "@/components/other/Icons/Avatar"; 9 | import Radio from "@/components/UI/Radio/Radio"; 10 | import {usersApi} from "@/store/api/users/users.api"; 11 | 12 | interface Props { 13 | modalShown: boolean; 14 | setModalShown: React.Dispatch>; 15 | user: UserType; 16 | } 17 | 18 | const InstructorChangeModal: FC = ({ 19 | modalShown, 20 | setModalShown, 21 | user 22 | }) => { 23 | const [role, setRole] = useState(""); 24 | 25 | const [changeRole] = usersApi.useChangeUserRoleMutation(); 26 | 27 | const handleTeacherChange = (event: FormEvent) => { 28 | event.preventDefault(); 29 | if (role !== "") { 30 | changeRole({ 31 | userId: user.id, 32 | body: { 33 | role: role 34 | } 35 | }).then(() => setModalShown(false)); 36 | } 37 | } 38 | 39 | return ( 40 | 45 |
46 |

Группа №{user.id}

47 | 50 |
51 |
52 |
53 | 54 |
55 |

{user.surname} {user.name[0]}.{" "} 56 | {user.patronymic && user.patronymic[0] + "."}

57 |

Учитель {user.role === UserRole.THEORY_TEACHER ? 'теории' : 'практики'}

58 |
59 |
60 |
handleTeacherChange(event)}> 61 | setRole(event.target.value)} 66 | /> 67 | setRole(event.target.value)} 72 | /> 73 | 74 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | export default InstructorChangeModal; 81 | -------------------------------------------------------------------------------- /src/components/UI/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, SelectHTMLAttributes } from "react"; 2 | 3 | import styles from "./Select.module.scss"; 4 | import classNames from "classnames"; 5 | 6 | export enum SelectTypes { 7 | Users, 8 | Categories, 9 | Groups, 10 | Schedule 11 | } 12 | 13 | interface Props extends SelectHTMLAttributes { 14 | title?: string; 15 | type?: SelectTypes; 16 | options: any[]; 17 | } 18 | 19 | const Select = forwardRef( 20 | ({ title, options, type, className, ...props }, ref) => { 21 | return ( 22 | 81 | ); 82 | } 83 | ); 84 | 85 | Select.displayName = "Select"; 86 | 87 | export default Select; 88 | -------------------------------------------------------------------------------- /src/components/pages/Contacts/ContactsSection/ContactsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import { motion } from "framer-motion"; 4 | 5 | import styles from "./ContactsSection.module.scss"; 6 | 7 | import InputSecondary from "../../../UI/Input/InputSecondary/InputSecondary"; 8 | import Button from "../../../UI/Button/Button"; 9 | import Arrow from "../../../other/Icons/Arrow"; 10 | import Phone from "../../../other/Icons/Phone"; 11 | import Mail from "../../../other/Icons/Mail"; 12 | import Geo from "../../../other/Icons/Geo"; 13 | 14 | const ContactsSection: FC = () => { 15 | return ( 16 | 22 |
23 |
24 |

Остались вопросы?

25 |

26 | Свяжитесь с нами, отправив ваше имя и номер телефона и в ближайшее 27 | время менеджер свяжется с вами и ответит на все ваши вопросы 28 |

29 |
30 |
31 |
32 | 33 | 38 |
39 | 42 |
43 |
44 |
45 |
Контактные данные
46 | 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default ContactsSection; 91 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupItem/GroupItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import styles from "./GroupItem.module.scss"; 4 | import Button from "@/components/UI/Button/Button"; 5 | import GroupAvatar from "@/components/other/Icons/GroupAvatar"; 6 | import GroupDeleteModal from "@/components/pages/Profile/Admin/GroupsSection/GroupDeleteModal/GroupDeleteModal"; 7 | import { AnimatePresence } from "framer-motion"; 8 | import { GroupType } from "@/store/api/groups/groups.types"; 9 | import GroupChangeModal from "@/components/pages/Profile/Admin/GroupsSection/GroupChangeModal/GroupChangeModal"; 10 | import {UserType} from "@/store/api/users/users.types"; 11 | import {CategoryType} from "@/store/api/categories/categories.types"; 12 | 13 | interface Props { 14 | group: GroupType; 15 | theoryTeachers: UserType[]; 16 | practiceTeachers: UserType[]; 17 | categories: CategoryType[]; 18 | } 19 | 20 | const GroupItem: FC = ({ group, theoryTeachers, practiceTeachers, categories }) => { 21 | const [modalDeleteShown, setModalDeleteShown] = useState(false); 22 | const [modalChangeShown, setModalChangeShown] = useState(false); 23 | 24 | return ( 25 |
26 |
27 | 28 |
29 |

Группа №{group.id}

30 |

31 | Категория: {group.category.value} 32 |

33 |
34 |
35 |
36 |
37 |

Учитель теории

38 |

39 | {group.theoryTeacher.surname} {group.theoryTeacher.name[0]}.{" "} 40 | {group.theoryTeacher.patronymic && 41 | group.theoryTeacher.patronymic[0] + "."} 42 |

43 |
44 |
45 |

Учитель практики

46 |

47 | {group.practiceTeacher.surname} {group.practiceTeacher.name[0]}.{" "} 48 | {group.practiceTeacher.patronymic && 49 | group.practiceTeacher.patronymic[0] + "."} 50 |

51 |
52 |
53 |
54 | 55 | 58 |
59 | 60 | {modalDeleteShown && ( 61 | 66 | )} 67 | {modalChangeShown && ( 68 | 76 | )} 77 | 78 |
79 | ); 80 | }; 81 | 82 | export default GroupItem; 83 | -------------------------------------------------------------------------------- /src/components/pages/Profile/Admin/GroupsSection/GroupCreateModal/GroupCreateModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent, useState } from "react"; 2 | import ExitThin from "@/components/other/Icons/ExitThin"; 3 | import GroupAvatar from "@/components/other/Icons/GroupAvatar"; 4 | import Select, { SelectTypes } from "@/components/UI/Select/Select"; 5 | import Button from "@/components/UI/Button/Button"; 6 | import ModalWrapper from "@/components/other/ModalWrapper/ModalWrapper"; 7 | 8 | import styles from "./GroupCreateModal.module.scss"; 9 | import { CategoryType } from "@/store/api/categories/categories.types"; 10 | import { UserType } from "@/store/api/users/users.types"; 11 | import { groupsApi } from "@/store/api/groups/groups.api"; 12 | 13 | interface Props { 14 | modalShown: boolean; 15 | setModalShown: React.Dispatch>; 16 | theoryTeachers: UserType[]; 17 | practiceTeachers: UserType[]; 18 | categories: CategoryType[]; 19 | } 20 | 21 | const GroupCreateModal: FC = ({ 22 | modalShown, 23 | setModalShown, 24 | theoryTeachers, 25 | practiceTeachers, 26 | categories 27 | }) => { 28 | const [theoryTeacherId, setTheoryTeacherId] = useState(0); 29 | const [practiceTeacherId, setPracticeTeacherId] = useState(0); 30 | const [categoryId, setCategoryId] = useState(0); 31 | 32 | const [createGroup] = groupsApi.useCreateGroupMutation(); 33 | 34 | const handleSubmit = (event: FormEvent) => { 35 | event.preventDefault(); 36 | if (theoryTeacherId && practiceTeacherId && categoryId) { 37 | createGroup({ 38 | theoryTeacherId: theoryTeacherId, 39 | practiceTeacherId: practiceTeacherId, 40 | categoryId: categoryId 41 | }).then(() => setModalShown(false)); 42 | } 43 | }; 44 | 45 | return ( 46 | 51 |
52 |

Создать группу

53 | 56 |
57 |
58 |
59 | 60 |

Новая группа

61 |
62 | {theoryTeachers && practiceTeachers && categories && ( 63 |
handleSubmit(event)}> 64 |
65 | setPracticeTeacherId(+event.target.value)} 75 | type={SelectTypes.Users} 76 | required 77 | options={practiceTeachers} 78 | /> 79 | setTheoryTeacherId(+event.target.value)} type={SelectTypes.Users} options={theoryTeachers}/> 75 | setCategoryId(+event.target.value)} type={SelectTypes.Categories} options={categories} /> 77 |
78 | 79 |
80 |
81 |

Ученики ({group.students.length})

82 |
    83 | {group.students.map((student) =>
  • )} 84 |
85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default GroupChangeModal; 92 | -------------------------------------------------------------------------------- /src/components/pages/Home/PrimarySection/PrimarySection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import styles from "./PrimarySection.module.scss"; 4 | import Button from "../../../UI/Button/Button"; 5 | 6 | import { Swiper, SwiperSlide } from "swiper/react"; 7 | import "swiper/css"; 8 | import { motion } from "framer-motion"; 9 | 10 | import Arrow from "../../../other/Icons/Arrow"; 11 | 12 | import Image from "next/image"; 13 | import { Autoplay } from "swiper"; 14 | import Link from "next/link"; 15 | 16 | import car from "/src/images/oversize/car.png"; 17 | import chart from "/src/images/icons/chart.png"; 18 | import fleet from "/src/images/icons/fleet.png"; 19 | import instructor from "/src/images/icons/instructor.png"; 20 | import discount from "/src/images/icons/discount.png"; 21 | 22 | const PrimarySection: FC = () => { 23 | return ( 24 | 30 |
31 |

Научитесь водить уже через 3 месяца

32 |

33 | Научитесь водить уже через 3 месяца. Предостовляем высококачественные 34 | уроки вождения с 2000 года и выпускаем более 450 студентов в месяц. 35 |

36 | 37 | 40 | 41 | Картинка машины 42 |
43 |
44 |
45 | График 46 |

График

47 |

Подстраивающийся под вас

48 |
49 |
50 | Автопарк 51 |

Автопарк

52 |

Ежегодно обновляется

53 |
54 |
55 | Инструктора 56 |

Инструктора

57 |

С опытом не менее 5 лет

58 |
59 |
60 | Рассрочка и скидки 61 |

Рассрочка и скидки

62 |

На 12 м. и скидки студентам

63 |
64 |
65 | 75 | 76 |
77 | График 78 |

График

79 |

Подстраивающийся под вас

80 |
81 |
82 | 83 |
84 | Автопарк 85 |

Автопарк

86 |

Ежегодно обновляется

87 |
88 |
89 | 90 |
91 | Инструктора 92 |

Инструктора

93 |

С опытом не менее 5 лет

94 |
95 |
96 | 97 |
98 | Рассрочка и скидки 99 |

Рассрочка и скидки

100 |

На 12 м. и скидки студентам

101 |
102 |
103 |
104 |
105 | ); 106 | }; 107 | 108 | export default PrimarySection; 109 | -------------------------------------------------------------------------------- /src/components/pages/Home/FormSection/FormSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { motion } from "framer-motion"; 3 | 4 | import styles from "./FormSection.module.scss"; 5 | import Heading from "../../../UI/Heading/Heading"; 6 | import InputSecondary from "../../../UI/Input/InputSecondary/InputSecondary"; 7 | import Radio from "../../../UI/Radio/Radio"; 8 | import LongArrow from "../../../other/Icons/LongArrow"; 9 | 10 | const FormSection: FC = () => { 11 | return ( 12 | 19 | Подробная заявка на обучение 20 |
21 |
22 | 29 | 37 | 45 | 53 |
54 |
55 |
56 |
Желаемые категории
57 |
58 | 65 | 72 | 79 | 86 |
87 |
88 |
89 |
Предпочитаемая группа
90 |
91 | 98 | 105 | 112 |
113 |
114 |
115 | 118 |
119 |
120 | ); 121 | }; 122 | 123 | export default FormSection; 124 | -------------------------------------------------------------------------------- /src/components/other/Icons/GroupAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | const GroupAvatar: FC = () => { 4 | return ( 5 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default GroupAvatar; 41 | -------------------------------------------------------------------------------- /src/components/other/Header/HeaderMobile/Dropdown/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | import Link from "next/link"; 4 | 5 | import styles from "./Dropdown.module.scss"; 6 | import Button from "../../../../UI/Button/Button"; 7 | import Logo from "../../../../UI/Logo/Logo"; 8 | import Cross from "../../../Icons/Cross"; 9 | import classNames from "classnames"; 10 | import { createFocusTrap } from "focus-trap"; 11 | import { useAuth } from "@/hooks/useAuth"; 12 | import { useRouter } from "next/router"; 13 | import { useTypedDispatch } from "@/hooks/useTypedDispatch"; 14 | import { logout } from "@/store/auth/auth.slice"; 15 | 16 | interface Props { 17 | isDropdown: boolean; 18 | setIsDropdown: React.Dispatch>; 19 | setIsModalShow: React.Dispatch>; 20 | } 21 | 22 | const Dropdown: FC = ({ setIsModalShow, isDropdown, setIsDropdown }) => { 23 | const ref = useRef(null); 24 | const dispatch = useTypedDispatch(); 25 | const { pathname } = useRouter(); 26 | 27 | const handleLogout = () => { 28 | dispatch(logout()); 29 | }; 30 | 31 | useEffect(() => { 32 | const trap = createFocusTrap(ref.current as HTMLDivElement, { 33 | allowOutsideClick: true 34 | }); 35 | 36 | if (isDropdown) trap.activate(); 37 | 38 | return () => { 39 | trap.deactivate(); 40 | }; 41 | }, [isDropdown]); 42 | 43 | return createPortal( 44 |
setIsDropdown(false)} 50 | > 51 |
event.stopPropagation()} 54 | ref={ref} 55 | > 56 |
57 | 60 | 63 |
64 | 89 |
90 | 97 | {useAuth() ? ( 98 | pathname.includes("profile") ? ( 99 | 106 | ) : ( 107 | 108 | 111 | 112 | ) 113 | ) : ( 114 | 121 | )} 122 |
123 |
124 |
, 125 | document.getElementById("overlay") as HTMLElement 126 | ); 127 | }; 128 | 129 | export default Dropdown; 130 | -------------------------------------------------------------------------------- /src/components/other/ModalWrapper/LoginModal/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styles from "./LoginModal.module.scss"; 3 | import ExitThin from "../../Icons/ExitThin"; 4 | import InputPrimary from "../../../UI/Input/InputPrimary/InputPrimary"; 5 | import Button from "../../../UI/Button/Button"; 6 | import Link from "next/link"; 7 | import * as Yup from "yup"; 8 | import { yupResolver } from "@hookform/resolvers/yup"; 9 | import { Controller, SubmitHandler, useForm } from "react-hook-form"; 10 | import { useRouter } from "next/navigation"; 11 | import { authApi } from "@/store/api/auth/auth.api"; 12 | import { LoginType } from "@/store/api/auth/auth.types"; 13 | 14 | interface Props { 15 | setModalType: React.Dispatch>; 16 | setIsModalShow: React.Dispatch>; 17 | } 18 | 19 | interface LoginFields extends LoginType {} 20 | 21 | const LoginModal: FC = ({ setIsModalShow, setModalType }) => { 22 | const router = useRouter(); 23 | const [login] = authApi.useLoginMutation(); 24 | 25 | const formSchema = Yup.object().shape({ 26 | email: Yup.string() 27 | .required("Введите E-mail") 28 | .matches( 29 | /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/, 30 | "Введите валидный E-mail" 31 | ), 32 | password: Yup.string() 33 | .required("Введите пароль") 34 | .min(8, "Пароль должен содержать минимум 8 символов") 35 | }); 36 | const validationOpt = { resolver: yupResolver(formSchema) }; 37 | 38 | const { 39 | handleSubmit, 40 | control, 41 | setError, 42 | formState: { errors } 43 | } = useForm(validationOpt); 44 | 45 | const onSubmit: SubmitHandler = (data) => { 46 | login(data) 47 | .then((data: any) => { 48 | if (data.error) { 49 | if (data.error.status === 404) { 50 | setError("email", { 51 | message: "ㅤ" 52 | }); 53 | setError("password", { 54 | message: "Ошибка сервера. Попробуйте позже" 55 | }); 56 | } 57 | switch (data.error.data.field) { 58 | case "password": 59 | setError("password", { 60 | message: data.error.data.error 61 | }); 62 | break; 63 | case "all": 64 | setError("email", { 65 | message: "ㅤ" 66 | }); 67 | setError("password", { 68 | message: data.error.data.error 69 | }); 70 | break; 71 | default: 72 | setError("email", { 73 | message: "ㅤ" 74 | }); 75 | setError("password", { 76 | message: "Ошибка сервера. Попробуйте позже" 77 | }); 78 | break; 79 | } 80 | } else { 81 | router.push("/profile"); 82 | } 83 | }) 84 | .catch((error) => console.log("Error", error)); 85 | }; 86 | 87 | return ( 88 |
89 |
90 |

Вход

91 | 94 |
95 |
96 | ( 101 | 107 | )} 108 | /> 109 | ( 114 | 120 | )} 121 | /> 122 | 125 | 126 |
127 |

128 | Новый пользователь?{" "} 129 | 132 |

133 |

134 | Ввод данных подтверждает ваше согласие с{" "} 135 | политикой конфиденциальности и{" "} 136 | обработкой персональных данных. 137 |

138 |
139 |
140 | ); 141 | }; 142 | 143 | export default LoginModal; 144 | --------------------------------------------------------------------------------