├── .gitignore ├── README.md ├── backend ├── .env ├── .gitignore ├── config │ └── db.js ├── controllers │ ├── clientController.js │ ├── groupProps.js │ └── whiteListProps.js ├── data │ └── clients.js ├── middleware │ └── errorMiddleware.js ├── models │ └── clientModel.js ├── package.json ├── routes │ ├── clientRoutes.js │ └── clientsRoutes.js ├── seeder.js ├── server.js └── yarn.lock └── client ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── css ├── base.css ├── button.css ├── contacts.css ├── container.css ├── dropdown.css ├── filtration.css ├── fonts.css ├── form-errors.css ├── form.css ├── header.css ├── heading.css ├── popup.css ├── styles.css ├── table.css ├── toast.css └── variables.css ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts ├── OpenSans-Bold.woff ├── OpenSans-Bold.woff2 ├── OpenSans-BoldItalic.woff ├── OpenSans-BoldItalic.woff2 ├── OpenSans-ExtraBold.woff ├── OpenSans-ExtraBold.woff2 ├── OpenSans-ExtraBoldItalic.woff ├── OpenSans-ExtraBoldItalic.woff2 ├── OpenSans-Italic.woff ├── OpenSans-Italic.woff2 ├── OpenSans-Light.woff ├── OpenSans-Light.woff2 ├── OpenSans-LightItalic.woff ├── OpenSans-LightItalic.woff2 ├── OpenSans-Regular.woff ├── OpenSans-Regular.woff2 ├── OpenSans-SemiBold.woff ├── OpenSans-SemiBold.woff2 ├── OpenSans-SemiBoldItalic.woff └── OpenSans-SemiBoldItalic.woff2 ├── images ├── add-circle.svg ├── add-contact.svg ├── angle.svg ├── arrow.svg ├── button-loader.svg ├── cancel-button-red.svg ├── cancel-button.svg ├── cancel.svg ├── close-button.svg ├── edit.svg ├── fb.svg ├── loader.svg ├── logo.svg ├── mail.svg ├── phone.svg ├── user-contact.svg └── vk.svg ├── index.html ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── safari-pinned-tab.svg ├── scripts ├── api │ ├── api.js │ └── provider.js ├── const.js ├── main.js ├── models │ ├── clients-model.js │ ├── filter-model.js │ ├── form-model.js │ └── search-model.js ├── presenters │ └── clients-presenter.js ├── utils │ ├── common.js │ ├── debounce.js │ ├── observer.js │ ├── render.js │ ├── router.js │ └── validate.js └── views │ ├── abstract-view.js │ ├── app-view.js │ ├── button-add-client.js │ ├── button-add-contact.js │ ├── button-filled.js │ ├── button-link.js │ ├── client-profile-view.js │ ├── clients-view.js │ ├── dropdown-view.js │ ├── errors-container.js │ ├── filtering-list-view.js │ ├── form-container.js │ ├── form-errors.js │ ├── header-container.js │ ├── header-view.js │ ├── input-view.js │ ├── loading-view.js │ ├── main-container.js │ ├── modal-delete-client.js │ ├── modal-person-info.js │ ├── modal-title.js │ ├── modal-view.js │ ├── no-clients-view.js │ ├── no-search-clients-view.js │ ├── pagination-view.js │ ├── search-profile-view.js │ ├── smart.js │ ├── table-body-view.js │ └── table-head-view.js ├── site.webmanifest └── vendor └── imask.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Установка 2 | * Установите [Node.js](https://nodejs.org/en/download/) 3 | `brew install node` 4 | * Установите [yarn](https://yarnpkg.com/lang/en/docs/install/) 5 | `brew install yarn` 6 | * Перейдите в корневую директорию проекта `cd backend` 7 | * Установите зависимости проекта `yarn` 8 | 9 | ## Запуск проекта: 10 | `yarn start` 11 | 12 | ## Для разработки: 13 | `yarn server` 14 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | MONGO_URI="mongodb+srv://alvar91:1q2w3e4r5t6y@cluster0.plfdz.mongodb.net/skillbox-crm?retryWrites=true&w=majority" 2 | PORT=5000 -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import colors from "colors"; 3 | 4 | const connectDB = async () => { 5 | try { 6 | const conn = await mongoose.connect(process.env.MONGO_URI, { 7 | useUnifiedTopology: true, 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useFindAndModify: false, 11 | }); 12 | 13 | console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline); 14 | } catch (error) { 15 | console.error(`Error: ${error.message}`.red.underline.bold); 16 | process.exit(1); 17 | } 18 | }; 19 | 20 | export default connectDB; 21 | -------------------------------------------------------------------------------- /backend/controllers/clientController.js: -------------------------------------------------------------------------------- 1 | import asyncHandler from "express-async-handler"; 2 | import Client from "../models/clientModel.js"; 3 | 4 | import { whiteListProps } from "./whiteListProps.js"; 5 | import { groupStringProps, groupContactProps } from "./groupProps.js"; 6 | 7 | const parseData = (reqBody) => { 8 | const parsedData = { 9 | contacts: [], 10 | }; 11 | 12 | const bodyValues = Object.values(reqBody); 13 | 14 | for (const { name, value } of bodyValues) { 15 | if (!whiteListProps.includes(name)) continue; 16 | 17 | if (groupStringProps.includes(name)) { 18 | parsedData[name] = value; 19 | } 20 | 21 | if (groupContactProps.includes(name)) { 22 | parsedData.contacts.push({ type: name, value }); 23 | } 24 | } 25 | 26 | return parsedData; 27 | }; 28 | 29 | // @desc Register a new client 30 | // @route POST /api/clients 31 | // @access Public 32 | const registerClient = asyncHandler(async (req, res) => { 33 | const parsedData = parseData(req.body); 34 | 35 | // const clientExists = await Client.findById(req.params.id); 36 | 37 | // if (clientExists) { 38 | // res.status(400); 39 | // throw new Error("Client already exists"); 40 | // } 41 | 42 | const client = await Client.create(parsedData); 43 | 44 | if (client) { 45 | res.status(201).json({ 46 | _id: client._id, 47 | name: client.name, 48 | surname: client.surname, 49 | lastname: client.lastname, 50 | contacts: client.contacts, 51 | }); 52 | } else { 53 | res.status(400); 54 | throw new Error("Invalid client data"); 55 | } 56 | }); 57 | 58 | // @desc Get all clients by pages 59 | // @route GET /api/clients?pageNumber=1 60 | // @access Public 61 | const getClients = asyncHandler(async (req, res) => { 62 | const pageSize = 10; 63 | const page = Number(req.query.pageNumber) || 1; 64 | 65 | const clients = await Client.find({}) 66 | .sort({ updatedAt: -1 }) 67 | .limit(pageSize) 68 | .skip(pageSize * (page - 1)); 69 | 70 | const count = await Client.countDocuments({}); 71 | 72 | res.json({ clients, page, pages: Math.ceil(count / pageSize) }); 73 | }); 74 | 75 | // @desc Get all clients by keyword 76 | // @route GET /api/clients/find?keyword 77 | // @access Public 78 | const getKeywordClients = asyncHandler(async (req, res) => { 79 | // const pageSize = 10; 80 | // const page = Number(req.query.pageNumber) || 1; 81 | 82 | const lowerKeyword = req.query.keyword?.toLowerCase(); 83 | const keyword = lowerKeyword 84 | ? { 85 | $or: [ 86 | { 87 | name: { 88 | $regex: lowerKeyword, 89 | $options: "i", 90 | }, 91 | }, 92 | { 93 | surname: { 94 | $regex: lowerKeyword, 95 | $options: "i", 96 | }, 97 | }, 98 | { 99 | lastname: { 100 | $regex: lowerKeyword, 101 | $options: "i", 102 | }, 103 | }, 104 | ], 105 | } 106 | : {}; 107 | 108 | //const count = await Client.countDocuments({ ...keyword }); 109 | const clients = await Client.find({ ...keyword }); 110 | // .limit(pageSize) 111 | // .skip(pageSize * (page - 1)); 112 | 113 | res.json({ clients }); 114 | }); 115 | 116 | // @desc Delete client 117 | // @route DELETE /api/client/:id 118 | // @access Public 119 | const deleteClient = asyncHandler(async (req, res) => { 120 | const client = await Client.findById(req.params.id); 121 | 122 | if (client) { 123 | await client.remove(); 124 | res.json({ message: "Client removed" }); 125 | } else { 126 | res.status(404); 127 | throw new Error("Client not found"); 128 | } 129 | }); 130 | 131 | // @desc Get client by ID 132 | // @route GET /api/client/:id 133 | // @access Public 134 | const getClientById = asyncHandler(async (req, res) => { 135 | const client = await Client.findById(req.params.id); 136 | if (client) { 137 | res.json(client); 138 | } else { 139 | res.status(404); 140 | throw new Error("Client not found"); 141 | } 142 | }); 143 | 144 | // @desc Update client 145 | // @route PUT /api/client/:id 146 | // @access Public 147 | const updateClient = asyncHandler(async (req, res) => { 148 | try { 149 | let client = await Client.findById(req.params.id); 150 | 151 | if (client) { 152 | const parsedData = parseData(req.body); 153 | 154 | await Client.findByIdAndUpdate(req.params.id, parsedData); 155 | 156 | const updatedClient = await Client.findById(req.params.id); 157 | res.json(updatedClient); 158 | } else { 159 | res.status(404); 160 | throw new Error("Client not found"); 161 | } 162 | } catch (e) { 163 | console.error(e.message); 164 | } 165 | }); 166 | 167 | export { 168 | registerClient, 169 | getClients, 170 | deleteClient, 171 | getClientById, 172 | updateClient, 173 | getKeywordClients, 174 | }; 175 | -------------------------------------------------------------------------------- /backend/controllers/groupProps.js: -------------------------------------------------------------------------------- 1 | export const groupStringProps = ["surname", "name", "lastname"]; 2 | 3 | export const groupContactProps = [ 4 | "phone", 5 | "additionalPhone", 6 | "email", 7 | "vk", 8 | "fb", 9 | "other", 10 | ]; 11 | -------------------------------------------------------------------------------- /backend/controllers/whiteListProps.js: -------------------------------------------------------------------------------- 1 | export const whiteListProps = [ 2 | "surname", 3 | "name", 4 | "lastname", 5 | "phone", 6 | "additionalPhone", 7 | "email", 8 | "vk", 9 | "fb", 10 | "other", 11 | ]; 12 | -------------------------------------------------------------------------------- /backend/data/clients.js: -------------------------------------------------------------------------------- 1 | const randomString = () => 2 | Math.random().toString(36).substring(10, 15) + 3 | Math.random().toString(36).substring(10, 15); 4 | 5 | const clients = []; 6 | for (let i = 0; i < 40; i++) { 7 | const client = { 8 | name: randomString(), 9 | surname: randomString(), 10 | lastname: randomString(), 11 | contacts: [ 12 | { type: "other", value: "@alvar91" }, 13 | { type: "email", value: "aleksey91scorp@bk.ru" }, 14 | { type: "vk", value: "alvar91" }, 15 | { type: "fb", value: "alvar91" }, 16 | { type: "phone", value: "+0(000)000-00-00" }, 17 | { type: "other", value: "@alvar91" }, 18 | { type: "other", value: "@alvar91" }, 19 | { type: "other", value: "@alvar91" }, 20 | { type: "other", value: "@alvar91" }, 21 | { type: "other", value: "@alvar91" }, 22 | ], 23 | }; 24 | 25 | clients.push(client); 26 | } 27 | 28 | export default clients; 29 | -------------------------------------------------------------------------------- /backend/middleware/errorMiddleware.js: -------------------------------------------------------------------------------- 1 | const notFound = (req, res, next) => { 2 | const error = new Error(`Not Found - ${req.originalUrl}`); 3 | res.status(404); 4 | next(error); 5 | }; 6 | 7 | const errorHandler = (err, req, res, next) => { 8 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode; 9 | res.status(statusCode); 10 | res.json({ 11 | message: err.message, 12 | stack: process.env.NODE_ENV === "production" ? null : err.stack, 13 | }); 14 | }; 15 | 16 | export { notFound, errorHandler }; 17 | -------------------------------------------------------------------------------- /backend/models/clientModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const clientSchema = mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | trim: true, 8 | lowercase: true, 9 | maxlength: 100, 10 | required: [true, "Please tell us your name!"], 11 | }, 12 | surname: { 13 | type: String, 14 | trim: true, 15 | lowercase: true, 16 | maxlength: 100, 17 | required: [true, "Please tell us your surname!"], 18 | }, 19 | lastname: { 20 | type: String, 21 | trim: true, 22 | lowercase: true, 23 | maxlength: 100, 24 | }, 25 | contacts: { 26 | type: [{ type: { type: String }, value: String }], 27 | trim: true, 28 | }, 29 | }, 30 | { 31 | timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" }, 32 | versionKey: false, 33 | } 34 | ); 35 | 36 | const Client = mongoose.model("Client", clientSchema); 37 | 38 | export default Client; 39 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skillbox-crm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node server", 9 | "server": "nodemon server", 10 | "client": "npm start --prefix frontend", 11 | "dev": "concurrently \"npm run server\" \"npm run client\"", 12 | "data:import": "node seeder", 13 | "data:destroy": "node seeder -d" 14 | }, 15 | "author": "Aleksey Varov", 16 | "license": "MIT", 17 | "dependencies": { 18 | "bcryptjs": "^2.4.3", 19 | "colors": "^1.4.0", 20 | "cors": "^2.8.5", 21 | "dotenv": "^8.2.0", 22 | "express": "^4.17.1", 23 | "express-async-handler": "^1.1.4", 24 | "jsonwebtoken": "^8.5.1", 25 | "mongoose": "^5.10.6", 26 | "morgan": "^1.10.0", 27 | "multer": "^1.4.2" 28 | }, 29 | "devDependencies": { 30 | "concurrently": "^5.3.0", 31 | "nodemon": "^2.0.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/routes/clientRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | const router = express.Router(); 3 | import { 4 | deleteClient, 5 | getClientById, 6 | updateClient, 7 | } from "../controllers/clientController.js"; 8 | 9 | router.route("/:id").put(updateClient).delete(deleteClient).get(getClientById); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /backend/routes/clientsRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | const router = express.Router(); 3 | import { 4 | registerClient, 5 | getClients, 6 | getKeywordClients 7 | } from "../controllers/clientController.js"; 8 | 9 | router.route("/").post(registerClient).get(getClients); 10 | 11 | router.route("/find").get(getKeywordClients); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /backend/seeder.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import dotenv from "dotenv"; 3 | import colors from "colors"; 4 | import clients from "./data/clients.js"; 5 | import Client from "./models/clientModel.js"; 6 | import connectDB from "./config/db.js"; 7 | 8 | dotenv.config(); 9 | 10 | connectDB(); 11 | 12 | const importData = async () => { 13 | try { 14 | await Client.deleteMany(); 15 | 16 | await Client.insertMany(clients); 17 | 18 | console.log("Data Imported!".green.inverse); 19 | process.exit(); 20 | } catch (error) { 21 | console.error(`${error}`.red.inverse); 22 | process.exit(1); 23 | } 24 | }; 25 | 26 | const destroyData = async () => { 27 | try { 28 | await Client.deleteMany(); 29 | 30 | console.log("Data Destroyed!".red.inverse); 31 | process.exit(); 32 | } catch (error) { 33 | console.error(`${error}`.red.inverse); 34 | process.exit(1); 35 | } 36 | }; 37 | 38 | if (process.argv[2] === "-d") { 39 | destroyData(); 40 | } else { 41 | importData(); 42 | } 43 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | import { fileURLToPath } from 'url'; 4 | import dotenv from "dotenv"; 5 | import morgan from "morgan"; 6 | import { notFound, errorHandler } from "./middleware/errorMiddleware.js"; 7 | import connectDB from "./config/db.js"; 8 | import colors from "colors"; 9 | import cors from "cors"; 10 | 11 | import clientRoutes from "./routes/clientRoutes.js"; 12 | import clientsRoutes from "./routes/clientsRoutes.js"; 13 | 14 | dotenv.config(); 15 | 16 | connectDB(); 17 | 18 | const app = express(); 19 | 20 | if (process.env.NODE_ENV === "development") { 21 | app.use(morgan("dev")); 22 | } 23 | 24 | app.use(express.json()); 25 | 26 | // Implement CORS 27 | app.use(cors()); 28 | 29 | app.options("*", cors()); 30 | 31 | app.use("/api/client", clientRoutes); 32 | app.use("/api/clients", clientsRoutes); 33 | 34 | const __filename = fileURLToPath(import.meta.url); 35 | const __dirname = path.dirname(__filename); 36 | 37 | app.use(express.static(path.join(__dirname, "../client"))); 38 | 39 | app.get("*", (req, res) => { 40 | res.sendFile(path.resolve(__dirname, "../client", "index.html")); 41 | }); 42 | 43 | app.use(notFound); 44 | app.use(errorHandler); 45 | 46 | const PORT = process.env.PORT || 5000; 47 | 48 | app.listen(PORT, console.log(`Server running on http://localhost:${PORT}/`.yellow.bold)); 49 | -------------------------------------------------------------------------------- /client/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/android-chrome-512x512.png -------------------------------------------------------------------------------- /client/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/apple-touch-icon.png -------------------------------------------------------------------------------- /client/css/base.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding-bottom: 40px; 8 | font-family: "Open Sans", "Arial", sans-serif; 9 | font-size: 16px; 10 | line-height: 32px; 11 | color: var(--dark); 12 | background-color: var(--light-grey); 13 | } 14 | 15 | a { 16 | color: inherit; 17 | text-decoration: none; 18 | transition: color 0.2s ease; 19 | } 20 | 21 | a:hover { 22 | color: var(--firm); 23 | transition: color 0.2s ease; 24 | } 25 | 26 | a:active { 27 | color: var(--orange); 28 | transition: color 0.2s ease; 29 | } 30 | 31 | ul { 32 | list-style: none; 33 | } 34 | 35 | .icon { 36 | vertical-align: middle; 37 | } 38 | 39 | @media (min-width: 1024px) { 40 | .brake { 41 | display: none; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/css/button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | padding: 14px 27px; 3 | border: 1px solid var(--firm); 4 | font-family: "Open Sans", "Arial", sans-serif; 5 | font-size: 14px; 6 | line-height: 19px; 7 | font-weight: 600; 8 | color: var(--firm); 9 | background-color: var(--light-grey); 10 | transition: color 0.2s ease, background-color 0.2s ease; 11 | cursor: pointer; 12 | } 13 | 14 | .button-contact { 15 | margin-top: 20px; 16 | margin-bottom: 20px; 17 | border: none; 18 | font-family: Open Sans; 19 | font-size: 14px; 20 | font-style: normal; 21 | font-weight: 600; 22 | line-height: 19px; 23 | background-color: transparent; 24 | cursor: pointer; 25 | transition: color 0.2s ease; 26 | } 27 | 28 | .button-contact:hover { 29 | color: var(--firm); 30 | transition: color 0.2s ease; 31 | } 32 | 33 | .button:hover, 34 | .button:focus { 35 | color: var(--light-grey); 36 | border: 1px solid var(--light-firm); 37 | background-color: var(--light-firm); 38 | cursor: pointer; 39 | transition: color 0.2s ease, background-color 0.2s ease; 40 | } 41 | 42 | .button:active { 43 | background-color: var(--firm); 44 | cursor: pointer; 45 | transition: background-color 0.2s ease; 46 | } 47 | 48 | .button:disabled { 49 | color: var(--light); 50 | border: none; 51 | background-color: var(--grey); 52 | cursor: not-allowed; 53 | } 54 | 55 | .button__add-icon { 56 | vertical-align: middle; 57 | transition: fill 0.2s ease; 58 | } 59 | 60 | .button:hover, .button:active .button__add-icon path { 61 | fill: var(--light-grey); 62 | transition: fill 0.2s ease; 63 | } 64 | 65 | .button-filled:disabled { 66 | color: var(--light); 67 | border: none; 68 | background-color: var(--grey); 69 | cursor: not-allowed; 70 | } 71 | 72 | .button-filled { 73 | background-color: var(--firm); 74 | } 75 | 76 | .button-filled--pagination { 77 | padding: 5px 10px; 78 | color: var(--light-grey); 79 | } 80 | 81 | .button-filled--save { 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | margin-bottom: 12px; 86 | width: 147px; 87 | height: 44px; 88 | border: none; 89 | color: var(--light); 90 | font-size: 14px; 91 | font-weight: 600; 92 | line-height: 19px; 93 | text-align: center; 94 | cursor: pointer; 95 | } 96 | 97 | .button-link { 98 | margin-bottom: 20px; 99 | padding: 0; 100 | border: none; 101 | border-bottom: 1px solid var(--dark); 102 | font-size: 12px; 103 | font-weight: 400; 104 | line-height: 10px; 105 | letter-spacing: 0px; 106 | background-color: transparent; 107 | cursor: pointer; 108 | } 109 | 110 | @keyframes rotate { 111 | to { 112 | transform: rotate(360deg); 113 | } 114 | } 115 | 116 | .button-loader { 117 | align-self: center; 118 | margin: 6px; 119 | width: 16px; 120 | height: 16px; 121 | background: url("../images/button-loader.svg") center center no-repeat; 122 | animation: rotate 1.4s linear infinite; 123 | } -------------------------------------------------------------------------------- /client/css/contacts.css: -------------------------------------------------------------------------------- 1 | .contacts { 2 | display: flex; 3 | flex-wrap: wrap; 4 | width: 108px; 5 | padding: 10px 0; 6 | margin: 0; 7 | } 8 | 9 | .contacts__item { 10 | margin-right: 7px; 11 | } 12 | 13 | .contacts__item:nth-child(5n) { 14 | margin-right: 0; 15 | } 16 | 17 | .tooltip { 18 | outline: none; 19 | text-decoration: none; 20 | position: relative; 21 | } 22 | 23 | .tooltip g { 24 | opacity: 0.7; 25 | transition: opacity 0.2s ease; 26 | } 27 | 28 | .tooltip:hover g { 29 | opacity: 1; 30 | transition: opacity 0.2s ease; 31 | } 32 | 33 | .tooltip:hover .label { 34 | position: absolute; 35 | left: 5px; 36 | bottom: 20px; 37 | display: block; 38 | min-width: 150px; 39 | transform: translateX(-50%); 40 | z-index: 999; 41 | } 42 | 43 | .label { 44 | display: none; 45 | padding: 8px 17px; 46 | font-size: 12px; 47 | font-weight: 400; 48 | line-height: 16px; 49 | letter-spacing: 0em; 50 | text-align: center; 51 | color: var(--light); 52 | background: var(--dark); 53 | } 54 | 55 | .tooltip:hover .label--phone { 56 | min-width: 220px; 57 | } 58 | 59 | .label:after { 60 | content: ""; 61 | position: absolute; 62 | left: 50%; 63 | bottom: -6px; 64 | display: block; 65 | border-color: var(--dark) transparent; 66 | border-style: solid; 67 | border-width: 6px 4px 0; 68 | } 69 | 70 | .tag { 71 | font-size: 12px; 72 | font-style: normal; 73 | font-weight: 700; 74 | line-height: 16px; 75 | letter-spacing: 0em; 76 | text-decoration: underline; 77 | color: var(--firm); 78 | } 79 | -------------------------------------------------------------------------------- /client/css/container.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 0px auto; 3 | padding: 0 20px; 4 | width: 984px; 5 | } 6 | 7 | .button-container { 8 | display: flex; 9 | justify-content: center; 10 | } 11 | 12 | .contact-container { 13 | display: flex; 14 | justify-content: center; 15 | background-color: var(--grey-form); 16 | } 17 | 18 | .errors-container { 19 | display: flex; 20 | justify-content: center; 21 | padding: 8px 30px; 22 | } 23 | -------------------------------------------------------------------------------- /client/css/dropdown.css: -------------------------------------------------------------------------------- 1 | .dropdown-container { 2 | position: relative; 3 | width: 123px; 4 | border: 1px solid var(--grey); 5 | } 6 | 7 | .dropdown-toggle { 8 | position: relative; 9 | padding: 10px 12px; 10 | font-size: 12px; 11 | font-style: normal; 12 | font-weight: 400; 13 | line-height: 16px; 14 | color: var(--dark); 15 | cursor: pointer; 16 | background-color: var(--light-grey); 17 | transition: all ease-in-out 0.3s; 18 | } 19 | 20 | .dropdown-toggle:hover, .dropdown-toggle:active, .dropdown-toggle:focus { 21 | background-color: var(--dropdown-active); 22 | color: var(--dark); 23 | } 24 | 25 | .dropdown-menu { 26 | position: absolute; 27 | left: -1px; 28 | display: none; 29 | width: 123px; 30 | border: 1px solid var(--grey); 31 | background-color: var(--light-grey); 32 | z-index: 10; 33 | } 34 | 35 | .dropdown-group { 36 | list-style: none; 37 | padding: 0; 38 | margin: 0; 39 | z-index: 10; 40 | } 41 | 42 | .dropdown-item { 43 | display: block; 44 | padding: 4px 11px 7px; 45 | line-height: 16px; 46 | text-decoration: none; 47 | color: var(--dark); 48 | font-size: 12px; 49 | font-style: normal; 50 | font-weight: 400; 51 | background-color: var(--dropdown-item); 52 | transition: all ease-in-out 0.3s; 53 | cursor: pointer; 54 | } 55 | 56 | .dropdown-item:hover, .dropdown-item:active, .dropdown-item:focus { 57 | background-color: var(--dropdown-active); 58 | color: var(--dark); 59 | } 60 | 61 | .dropdown-menu, .dropdown-toggle { 62 | position: relative; 63 | } 64 | 65 | .dropdown-toggle::before { 66 | content: ""; 67 | position: absolute; 68 | right: 12px; 69 | top: 40%; 70 | width: 12px; 71 | height: 12px; 72 | background: url("../images/angle.svg") no-repeat center center; 73 | margin-top: -2.5px; 74 | background-color: rgba(0, 0, 0, 0); 75 | transition: all ease-in-out 0.2s; 76 | } 77 | 78 | .dropdown-menu { 79 | position: relative; 80 | z-index: 10; 81 | } 82 | 83 | .dropdown-open .dropdown-menu.dropdown-active { 84 | position: absolute; 85 | display: block; 86 | } 87 | 88 | .dropdown-open .dropdown-toggle { 89 | color: var(--dark); 90 | } 91 | 92 | .dropdown-open .dropdown-toggle:before { 93 | transform: rotate(-180deg); 94 | } 95 | -------------------------------------------------------------------------------- /client/css/filtration.css: -------------------------------------------------------------------------------- 1 | .filtration__input { 2 | border: 1px solid rgba(51, 51, 51, 0.2); 3 | font-size: 14px; 4 | font-style: normal; 5 | font-weight: 400; 6 | line-height: 19px; 7 | letter-spacing: 0em; 8 | text-align: left; 9 | color: var(--dark); 10 | } -------------------------------------------------------------------------------- /client/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Open Sans"; 3 | src: url("../fonts/OpenSans-BoldItalic.woff2") format("woff2"), 4 | url("../fonts/OpenSans-BoldItalic.woff") format("woff"); 5 | font-weight: bold; 6 | font-style: italic; 7 | font-display: swap; 8 | } 9 | 10 | @font-face { 11 | font-family: "Open Sans"; 12 | src: url("../fonts/OpenSans-Bold.woff2") format("woff2"), 13 | url("../fonts/OpenSans-Bold.woff") format("woff"); 14 | font-weight: bold; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | 19 | @font-face { 20 | font-family: "Open Sans"; 21 | src: url("../fonts/OpenSans-ExtraBold.woff2") format("woff2"), 22 | url("../fonts/OpenSans-ExtraBold.woff") format("woff"); 23 | font-weight: 800; 24 | font-style: normal; 25 | font-display: swap; 26 | } 27 | 28 | @font-face { 29 | font-family: "Open Sans"; 30 | src: url("../fonts/OpenSans-Italic.woff2") format("woff2"), 31 | url("../fonts/OpenSans-Italic.woff") format("woff"); 32 | font-weight: normal; 33 | font-style: italic; 34 | font-display: swap; 35 | } 36 | 37 | @font-face { 38 | font-family: "Open Sans"; 39 | src: url("../fonts/OpenSans-Regular.woff2") format("woff2"), 40 | url("../fonts/OpenSans-Regular.woff") format("woff"); 41 | font-weight: normal; 42 | font-style: normal; 43 | font-display: swap; 44 | } 45 | 46 | @font-face { 47 | font-family: "Open Sans"; 48 | src: url("../fonts/OpenSans-SemiBold.woff2") format("woff2"), 49 | url("../fonts/OpenSans-SemiBold.woff") format("woff"); 50 | font-weight: 600; 51 | font-style: normal; 52 | font-display: swap; 53 | } 54 | 55 | @font-face { 56 | font-family: "Open Sans"; 57 | src: url("../fonts/OpenSans-Light.woff2") format("woff2"), 58 | url("../fonts/OpenSans-Light.woff") format("woff"); 59 | font-weight: 300; 60 | font-style: normal; 61 | font-display: swap; 62 | } 63 | 64 | @font-face { 65 | font-family: "Open Sans"; 66 | src: url("../fonts/OpenSans-ExtraBoldItalic.woff2") format("woff2"), 67 | url("../fonts/OpenSans-ExtraBoldItalic.woff") format("woff"); 68 | font-weight: 800; 69 | font-style: italic; 70 | font-display: swap; 71 | } 72 | 73 | @font-face { 74 | font-family: "Open Sans"; 75 | src: url("../fonts/OpenSans-SemiBoldItalic.woff2") format("woff2"), 76 | url("../fonts/OpenSans-SemiBoldItalic.woff") format("woff"); 77 | font-weight: 600; 78 | font-style: italic; 79 | font-display: swap; 80 | } 81 | 82 | @font-face { 83 | font-family: "Open Sans"; 84 | src: url("../fonts/OpenSans-LightItalic.woff2") format("woff2"), 85 | url("../fonts/OpenSans-LightItalic.woff") format("woff"); 86 | font-weight: 300; 87 | font-style: italic; 88 | font-display: swap; 89 | } 90 | -------------------------------------------------------------------------------- /client/css/form-errors.css: -------------------------------------------------------------------------------- 1 | .form-errors__item { 2 | margin: 0; 3 | width: 255px; 4 | font-size: 10px; 5 | font-weight: 400; 6 | line-height: 14px; 7 | letter-spacing: 0px; 8 | text-align: center; 9 | color: var(--orange); 10 | } -------------------------------------------------------------------------------- /client/css/form.css: -------------------------------------------------------------------------------- 1 | .form__contact { 2 | display: flex; 3 | padding: 15px 30px; 4 | padding-bottom: 0; 5 | background-color: var(--grey-form); 6 | } 7 | 8 | .form__contact:nth-child(4) { 9 | padding-top: 25px; 10 | padding-bottom: 0; 11 | } 12 | 13 | .form__input-contact { 14 | padding: 8px 12px; 15 | width: 240px; 16 | border: 1px solid var(--grey); 17 | border-left: none; 18 | border-right: none; 19 | font-size: 14px; 20 | font-style: normal; 21 | font-weight: 400; 22 | line-height: 19px; 23 | background-color: var(--grey-form); 24 | } 25 | 26 | .form__input--error { 27 | border-bottom: 1px solid var(--orange) !important; 28 | } 29 | 30 | .form__input:focus { 31 | outline: none; 32 | } 33 | 34 | .form__input-button { 35 | position: relative; 36 | width: 27px; 37 | background: url("../images/cancel-button.svg") no-repeat center center; 38 | background-color: var(--light-grey); 39 | border: 1px solid var(--grey); 40 | cursor: pointer; 41 | transition: 0.3s; 42 | } 43 | 44 | .form__input-button:hover { 45 | background: url("../images/cancel-button-red.svg") no-repeat center center; 46 | background-color: var(--light-grey); 47 | border: 1px solid var(--orange); 48 | transition: 0.3s; 49 | } 50 | 51 | .form__input-close { 52 | position: absolute; 53 | left: 5px; 54 | top: 10px; 55 | } 56 | 57 | .form__group { 58 | position: relative; 59 | min-height: 42px; 60 | margin: 15px 30px; 61 | margin-bottom: 15px; 62 | } 63 | 64 | .form__input { 65 | width: 390px; 66 | border: 0; 67 | border-bottom: 1px solid var(--grey); 68 | font-size: 14px; 69 | font-weight: 600; 70 | line-height: 19px; 71 | background-color: transparent; 72 | } 73 | 74 | .form__input:focus { 75 | outline: none; 76 | } 77 | 78 | .form__label { 79 | position: absolute; 80 | left: 0; 81 | top: 0; 82 | color: var(--txt-color); 83 | font-size: 14px; 84 | font-weight: 600; 85 | z-index: -1; 86 | transition: 0.3s; 87 | } 88 | 89 | .form__label--focused ~ .form__label { 90 | top: -16px; 91 | font-size: 12px; 92 | transition: 0.3s; 93 | } 94 | 95 | .form__input:focus ~ .form__label { 96 | top: -16px; 97 | font-size: 12px; 98 | transition: 0.3s; 99 | } 100 | 101 | .form__required { 102 | color: var(--firm); 103 | } 104 | 105 | .form__person { 106 | padding: 30px; 107 | padding-top: 0; 108 | } 109 | 110 | .form__container { 111 | margin: 0; 112 | padding: 0; 113 | } 114 | 115 | .form__item-title { 116 | color: var(--txt-color); 117 | } -------------------------------------------------------------------------------- /client/css/header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 40px; 5 | min-height: 90px; 6 | min-width: 1024px; 7 | background-color: var(--light); 8 | box-shadow: 0px 9.03012px 27.0904px rgba(176, 190, 197, 0.32), 9 | 0px 3.38629px 5.64383px rgba(176, 190, 197, 0.32); 10 | } 11 | 12 | .header__logo { 13 | height: 50px; 14 | width: 50px; 15 | margin-right: 53px; 16 | } 17 | 18 | .header__inner { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | .header__input { 24 | padding: 13px 16px; 25 | width: 580px; 26 | border: 1px solid rgba(51, 51, 51, 0.2); 27 | font-size: 14px; 28 | font-style: normal; 29 | font-weight: 400; 30 | line-height: 19px; 31 | letter-spacing: 0em; 32 | text-align: left; 33 | } 34 | 35 | .header__search-container { 36 | position: relative; 37 | } 38 | 39 | .header__search { 40 | position: absolute; 41 | width: 580px; 42 | background-color: var(--light); 43 | z-index: 999; 44 | } -------------------------------------------------------------------------------- /client/css/heading.css: -------------------------------------------------------------------------------- 1 | .heading__level-1 { 2 | margin: 0; 3 | margin-bottom: 26px; 4 | font-weight: bold; 5 | font-size: 24px; 6 | line-height: 33px; 7 | color: var(--dark); 8 | } -------------------------------------------------------------------------------- /client/css/popup.css: -------------------------------------------------------------------------------- 1 | @keyframes showActiveCard { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 6 | 100% { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | .popup { 12 | display: block; 13 | position: fixed; 14 | left: 50%; 15 | top: 50%; 16 | width: 100%; 17 | height: 100%; 18 | background-color: rgba(0, 0, 0, 0.5); 19 | transform: translate(-50%, -50%); 20 | z-index: 999; 21 | overflow: auto; 22 | animation: showActiveCard 0.8s forwards; 23 | } 24 | .popup__content { 25 | display: flex; 26 | //padding: 15px 30px; 27 | flex-direction: column; 28 | align-items: center; 29 | position: absolute; 30 | top: 50%; 31 | left: 50%; 32 | width: 450px; 33 | transform: translate(-50%, -50%); 34 | background-color: var(--light); 35 | } 36 | 37 | .popup__button-close { 38 | position: absolute; 39 | right: 15px; 40 | top: 15px; 41 | width: 29px; 42 | height: 29px; 43 | padding: 0; 44 | border: 0; 45 | text-indent: -9999px; 46 | background: no-repeat 50% 50%; 47 | background-image: url("../images/close-button.svg"); 48 | z-index: 3; 49 | outline: none; 50 | cursor: pointer; 51 | } 52 | .popup__title { 53 | margin: 0; 54 | margin: 15px 30px; 55 | font-family: Open Sans; 56 | font-size: 18px; 57 | font-style: normal; 58 | font-weight: 700; 59 | line-height: 25px; 60 | letter-spacing: 0px; 61 | text-align: left; 62 | } 63 | 64 | .popup__id { 65 | padding-left: 9px; 66 | font-size: 12px; 67 | font-style: normal; 68 | font-weight: 400; 69 | line-height: 16px; 70 | color: var(--txt-color); 71 | } 72 | 73 | .popup-delete { 74 | display: flex; 75 | flex-direction: column; 76 | align-items: center; 77 | } 78 | 79 | .popup-delete__text { 80 | font-size: 14px; 81 | font-weight: 400; 82 | line-height: 19px; 83 | text-align: center; 84 | } 85 | 86 | .popup-delete__title { 87 | font-size: 18px; 88 | font-weight: 700; 89 | line-height: 25px; 90 | letter-spacing: 0px; 91 | } 92 | -------------------------------------------------------------------------------- /client/css/styles.css: -------------------------------------------------------------------------------- 1 | @import url("./fonts.css"); 2 | @import url("./variables.css"); 3 | @import url("./base.css"); 4 | @import url("./button.css"); 5 | @import url("./container.css"); 6 | @import url("./heading.css"); 7 | @import url("./header.css"); 8 | @import url("./table.css"); 9 | @import url("./filtration.css"); 10 | @import url("./contacts.css"); 11 | @import url("./popup.css"); 12 | @import url("./form.css"); 13 | @import url("./toast.css"); 14 | @import url("./dropdown.css"); 15 | @import url("./form-errors.css"); 16 | -------------------------------------------------------------------------------- /client/css/table.css: -------------------------------------------------------------------------------- 1 | .table { 2 | margin: 0 auto; 3 | margin-bottom: 40px; 4 | width: 984px; 5 | border-collapse: collapse; 6 | } 7 | 8 | .table_pagination { 9 | margin-bottom: 20px; 10 | } 11 | 12 | .table__thead { 13 | margin-bottom: 8px; 14 | font-size: 12px; 15 | font-style: normal; 16 | font-weight: 400; 17 | line-height: 16px; 18 | letter-spacing: 0px; 19 | text-align: left; 20 | color: var(--txt-color); 21 | } 22 | 23 | .table__body { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .table__inner { 29 | display: flex; 30 | align-items: flex-end; 31 | } 32 | 33 | .table__row { 34 | display: flex; 35 | align-items: center; 36 | 37 | min-height: 60px; 38 | border-bottom: 1px solid var(--grey); 39 | font-size: 14px; 40 | font-style: normal; 41 | font-weight: 400; 42 | line-height: 19px; 43 | letter-spacing: 0px; 44 | text-align: left; 45 | background-color: var(--light); 46 | transition: background-color 0.2s ease; 47 | } 48 | 49 | .table__row:hover { 50 | background-color: var(--active-grey); 51 | transition: background-color 0.2s ease; 52 | } 53 | 54 | .table__sort { 55 | color: var(--firm); 56 | } 57 | 58 | .table__pagination { 59 | margin-bottom: 20px; 60 | padding-left: 20px; 61 | } 62 | 63 | .table__loader { 64 | display: flex; 65 | min-height: 300px; 66 | background-color: var(--light); 67 | } 68 | 69 | @keyframes rotate { 70 | to { 71 | transform: rotate(360deg); 72 | } 73 | } 74 | 75 | .loader { 76 | align-self: center; 77 | margin: 0 auto; 78 | width: 100px; 79 | height: 100px; 80 | background: url("../images/loader.svg") center center no-repeat; 81 | animation: rotate 1.4s linear infinite; 82 | } 83 | 84 | .arrow-down { 85 | transform: rotate(180deg); 86 | } 87 | 88 | .table__id { 89 | padding-left: 20px; 90 | padding-right: 28px; 91 | width: 90px; 92 | color: var(--txt-color); 93 | } 94 | 95 | .table__name { 96 | width: 195px; 97 | } 98 | 99 | .table__date { 100 | margin-right: 7px; 101 | } 102 | 103 | .table__time { 104 | color: var(--txt-color); 105 | } 106 | 107 | .table__date { 108 | width: 128px; 109 | } 110 | 111 | .table__contacts { 112 | width: 108px; 113 | margin-right: 50px; 114 | } 115 | 116 | .table__actions { 117 | width: 85px; 118 | } 119 | 120 | .table__menu { 121 | min-width: 200px; 122 | } 123 | 124 | @media (min-width: 1024px) { 125 | .table__actions { 126 | width: 189px; 127 | } 128 | 129 | .table__date { 130 | width: 150px; 131 | } 132 | 133 | .table__menu { 134 | display: flex; 135 | min-width: 225px; 136 | } 137 | 138 | .table__edit { 139 | margin-right: 30px; 140 | } 141 | 142 | .table__row { 143 | } 144 | } 145 | 146 | .table-delete:hover { 147 | color: var(--orange); 148 | transition: color 0.2s ease; 149 | } -------------------------------------------------------------------------------- /client/css/toast.css: -------------------------------------------------------------------------------- 1 | .toast-container { 2 | position: fixed; 3 | z-index: 1000; 4 | top: 0; 5 | left: 50%; 6 | transform: translateX(-50%); 7 | padding: 8px; 8 | font-size: 18px; 9 | line-height: 24px; 10 | } 11 | 12 | .toast-item { 13 | padding: 8px; 14 | border-radius: 2px; 15 | background-color: #ffeeee; 16 | color: #900000; 17 | } 18 | -------------------------------------------------------------------------------- /client/css/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Colors */ 3 | --light: #FFF; 4 | --grey: #C8C5D1; 5 | --light-grey: #E5E5E5; 6 | --active-grey: #C8C5D1; 7 | --grey-form: #F4F3F6; 8 | --txt-color: #B0B0B0; 9 | --dark: #333; 10 | --firm: #9873FF; 11 | --light-firm: #c09cfc; 12 | --orange: #F06A4D; 13 | --dropdown-active: #C8C5D1; 14 | --dropdown-item: #E5E5E5; 15 | } 16 | -------------------------------------------------------------------------------- /client/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/favicon-16x16.png -------------------------------------------------------------------------------- /client/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/favicon-32x32.png -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/favicon.ico -------------------------------------------------------------------------------- /client/fonts/OpenSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Bold.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Bold.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-BoldItalic.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-BoldItalic.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-ExtraBold.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-ExtraBold.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Italic.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Italic.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Light.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Light.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-LightItalic.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-LightItalic.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Regular.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-Regular.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-SemiBold.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-SemiBold.woff2 -------------------------------------------------------------------------------- /client/fonts/OpenSans-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-SemiBoldItalic.woff -------------------------------------------------------------------------------- /client/fonts/OpenSans-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/fonts/OpenSans-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /client/images/add-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/add-contact.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/angle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/button-loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/cancel-button-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/cancel-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/close-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/fb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /client/images/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/images/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/images/user-contact.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/images/vk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | SKB CRM 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /client/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/mstile-144x144.png -------------------------------------------------------------------------------- /client/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/mstile-150x150.png -------------------------------------------------------------------------------- /client/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/mstile-310x150.png -------------------------------------------------------------------------------- /client/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/mstile-310x310.png -------------------------------------------------------------------------------- /client/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvar91/crm-skillbox-js-css-express-mongodb/1bc224210b969e3bb81d9cf9a7996825b078cff8/client/mstile-70x70.png -------------------------------------------------------------------------------- /client/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /client/scripts/api/api.js: -------------------------------------------------------------------------------- 1 | const RequestMethod = { 2 | GET: `GET`, 3 | POST: `POST`, 4 | PUT: `PUT`, 5 | DELETE: `DELETE`, 6 | }; 7 | 8 | const SuccessHTTPStatusRange = { 9 | MIN: 200, 10 | MAX: 299, 11 | }; 12 | 13 | export default class Api { 14 | constructor(endPoint) { 15 | this._endPoint = endPoint; 16 | } 17 | 18 | _load({ 19 | url, 20 | method = RequestMethod.GET, 21 | body = null, 22 | headers = new Headers(), 23 | }) { 24 | return fetch(`${this._endPoint}/${url}`, { method, body, headers }) 25 | .then(Api.checkStatus) 26 | .catch(Api.catchError); 27 | } 28 | 29 | addClient(client) { 30 | return this._load({ 31 | url: `clients`, 32 | method: RequestMethod.POST, 33 | body: JSON.stringify(client), 34 | headers: new Headers({ "Content-Type": `application/json` }), 35 | }) 36 | .then(Api.toJSON) 37 | .then(({ clients }) => { 38 | return { clients }; 39 | }); 40 | } 41 | 42 | static catchError(err) { 43 | throw err; 44 | } 45 | 46 | static checkStatus(response) { 47 | if ( 48 | response.status < SuccessHTTPStatusRange.MIN || 49 | response.status > SuccessHTTPStatusRange.MAX 50 | ) { 51 | throw new Error(`${response.status}: ${response.statusText}`); 52 | } 53 | 54 | return response; 55 | } 56 | 57 | deleteClient(clientId) { 58 | return this._load({ 59 | url: `client/${clientId}`, 60 | method: RequestMethod.DELETE, 61 | }); 62 | } 63 | 64 | getClients(currentPage = 1) { 65 | return this._load({ url: `clients?pageNumber=${currentPage}` }) 66 | .then(Api.toJSON) 67 | .then((clients) => clients); 68 | } 69 | 70 | searchClients(keyword) { 71 | return this._load({ url: `clients/find?keyword=${keyword}` }) 72 | .then(Api.toJSON) 73 | .then((clients) => clients); 74 | } 75 | 76 | getClient(clientId) { 77 | if (!clientId) Promise.reject(new Error(`Client id is required`)); 78 | 79 | return this._load({ url: `client/${clientId}` }) 80 | .then(Api.toJSON) 81 | .then((client) => client); 82 | } 83 | 84 | static toJSON(response) { 85 | return response.json(); 86 | } 87 | 88 | updateClient(client) { 89 | const _id = Object.values(client).find((item) => item.name === "_id").value; 90 | 91 | return this._load({ 92 | url: `client/${_id}`, 93 | method: RequestMethod.PUT, 94 | body: JSON.stringify(client), 95 | headers: new Headers({ "Content-Type": `application/json` }), 96 | }) 97 | .then(Api.toJSON) 98 | .then((response) => response); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /client/scripts/api/provider.js: -------------------------------------------------------------------------------- 1 | import Utils from "../utils/common.js"; 2 | 3 | export default class Provider { 4 | constructor(api, store) { 5 | this._api = api; 6 | } 7 | 8 | getClients(currentPage = 1) { 9 | if (Utils.isOnline()) { 10 | return this._api.getClients(currentPage).then((clients) => { 11 | return clients; 12 | }); 13 | } 14 | 15 | return Promise.reject(new Error(`Fetch clients failed`)); 16 | } 17 | 18 | searchClients(keyword) { 19 | if (Utils.isOnline()) { 20 | return this._api.searchClients(keyword).then((clients) => { 21 | return clients; 22 | }); 23 | } 24 | 25 | return Promise.reject(new Error(`Fetch clients failed`)); 26 | } 27 | 28 | getClient(clientId) { 29 | if (Utils.isOnline()) { 30 | return this._api.getClient(clientId).then((client) => { 31 | return client; 32 | }); 33 | } 34 | 35 | return Promise.reject(new Error(`Fetch client failed`)); 36 | } 37 | 38 | addClient(newClient) { 39 | if (Utils.isOnline()) { 40 | return this._api.addClient(newClient); 41 | } 42 | 43 | return Promise.reject(new Error(`Add client failed`)); 44 | } 45 | 46 | deleteClient(client) { 47 | if (Utils.isOnline()) { 48 | return this._api.deleteClient(client); 49 | } 50 | 51 | return Promise.reject(new Error(`Delete client failed`)); 52 | } 53 | 54 | updateClient(client) { 55 | if (Utils.isOnline()) { 56 | return this._api.updateClient(client).then((updatedClient) => { 57 | return updatedClient; 58 | }); 59 | } 60 | 61 | return Promise.reject(new Error(`Update client failed`)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/scripts/const.js: -------------------------------------------------------------------------------- 1 | import Utils from "./utils/common.js"; 2 | 3 | export const END_POINT = `http://localhost:5000/api`; 4 | 5 | export const RenderPosition = { 6 | AFTERBEGIN: `afterbegin`, 7 | BEFOREEND: `beforeend`, 8 | BEFOREBEGIN: `beforebegin`, 9 | AFTEREND: `afterend`, 10 | }; 11 | 12 | export const SortType = { 13 | ID_UP: `id-up`, 14 | ID_DOWN: `id-down`, 15 | NAME_UP: `name-up`, 16 | NAME_DOWN: `name-down`, 17 | DATE_CREATE_UP: `date-create-up`, 18 | DATE_CREATE_DOWN: `date-create-down`, 19 | DATE_UPDATE_UP: `date-update-up`, 20 | DATE_UPDATE_DOWN: `date-update-down`, 21 | }; 22 | 23 | export const UserAction = { 24 | ADD_CLIENT: `ADD_CLIENT`, 25 | EDIT_CLIENT: `EDIT_CLIENT`, 26 | DELETE_CLIENT: `DELETE_CLIENT`, 27 | }; 28 | 29 | export const UpdateType = { 30 | PATCH: `PATCH`, 31 | MINOR: `MINOR`, 32 | MINOR_FORM: `MINOR_FORM`, 33 | MAJOR: `MAJOR`, 34 | INIT: `INIT`, 35 | }; 36 | 37 | export const State = { 38 | ADDING: `ADDING`, 39 | DELETING: `DELETING`, 40 | ABORTING: `ABORTING`, 41 | }; 42 | 43 | export const SortingOrder = { 44 | POPULAR: `popular`, 45 | CHEAP: `cheap`, 46 | NEW: `new`, 47 | }; 48 | 49 | export const ContactImage = { 50 | VK: ` 51 | 52 | 53 | `, 54 | PHONE: ` 55 | 56 | 57 | 58 | 59 | 60 | `, 61 | MAIL: ` 62 | 63 | 64 | 65 | `, 66 | FB: ` 67 | 68 | 69 | 70 | 71 | `, 72 | USER: ` 73 | 74 | 75 | 76 | 77 | `, 78 | }; 79 | 80 | export const ContactURL = { 81 | VK: "https://vk.com/", 82 | FB: "https://www.facebook.com/", 83 | }; 84 | 85 | export const FORM = { 86 | inputText: "inputText", 87 | inputDropdown: "inputDropdown", 88 | }; 89 | 90 | export const MODE = { 91 | profile: "profile", 92 | addClient: "addClient", 93 | editClient: "editClient", 94 | deleteClient: "deleteClient", 95 | }; 96 | 97 | export const ButtonTitle = { 98 | save: "Сохранить", 99 | delete: "Удалить клиента", 100 | cancel: "Отмена", 101 | }; 102 | 103 | export const ModalTitle = { 104 | profile: "Профиль клиента", 105 | add: "Новый клиент", 106 | edit: "Изменить данные", 107 | delete: "Удалить клиента", 108 | }; 109 | 110 | export const FieldTitle = { 111 | surname: "Фамилия", 112 | name: "Имя", 113 | lastname: "Отчество", 114 | email: "Email", 115 | phone: "Телефон", 116 | additionalPhone: "Доп. телефон", 117 | vk: "Vk", 118 | fb: "Facebook", 119 | other: "Другое", 120 | }; 121 | 122 | export const Contact = { 123 | surname: "surname", 124 | name: "name", 125 | lastname: "lastname", 126 | email: "email", 127 | phone: "phone", 128 | additionalPhone: "additionalPhone", 129 | vk: "vk", 130 | fb: "fb", 131 | other: "other", 132 | default: "default", 133 | }; 134 | 135 | export const ValidationType = { 136 | notEmpty: "notEmpty", 137 | isEmail: "isEmail", 138 | isPhone: "isPhone", 139 | }; 140 | 141 | export const InitiateFields = () => { 142 | return [ 143 | { 144 | id: `${Utils.createUUID()}`, 145 | type: FORM.inputText, 146 | title: FieldTitle.surname, 147 | name: Contact.surname, 148 | value: "", 149 | required: true, 150 | validation: { 151 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.surname), 152 | }, 153 | isValid: false, 154 | isTouched: false, 155 | }, 156 | { 157 | id: `${Utils.createUUID()}`, 158 | type: FORM.inputText, 159 | title: FieldTitle.name, 160 | name: Contact.name, 161 | value: "", 162 | required: true, 163 | validation: { 164 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.name), 165 | }, 166 | isValid: false, 167 | isTouched: false, 168 | }, 169 | { 170 | id: `${Utils.createUUID()}`, 171 | type: FORM.inputText, 172 | title: FieldTitle.lastname, 173 | name: Contact.lastname, 174 | value: "", 175 | required: false, 176 | validation: { 177 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.lastname), 178 | }, 179 | isTouched: false, 180 | }, 181 | ]; 182 | }; 183 | 184 | const isTrue = (value) => { 185 | return value ? true : false; 186 | }; 187 | 188 | export class GetInputField { 189 | static getInputSurnameField = (value = "", id = `${Utils.createUUID()}`) => { 190 | return { 191 | id: id, 192 | type: FORM.inputText, 193 | title: FieldTitle.surname, 194 | name: Contact.surname, 195 | value: Utils.toUpperCaseFirstLetter(value), 196 | required: true, 197 | validation: { 198 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.surname), 199 | }, 200 | isValid: isTrue(value), 201 | isTouched: isTrue(value), 202 | }; 203 | }; 204 | 205 | static getInputNameField = (value = "", id = `${Utils.createUUID()}`) => { 206 | return { 207 | id: id, 208 | type: FORM.inputText, 209 | title: FieldTitle.name, 210 | name: Contact.name, 211 | value: Utils.toUpperCaseFirstLetter(value), 212 | required: true, 213 | validation: { 214 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.name), 215 | }, 216 | isValid: isTrue(value), 217 | isTouched: isTrue(value), 218 | }; 219 | }; 220 | 221 | static getInputLastnameField = (value = "", id = `${Utils.createUUID()}`) => { 222 | return { 223 | id: id, 224 | type: FORM.inputText, 225 | title: FieldTitle.lastname, 226 | name: Contact.lastname, 227 | value: Utils.toUpperCaseFirstLetter(value), 228 | required: false, 229 | validation: { 230 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.lastname), 231 | }, 232 | isValid: isTrue(value), 233 | isTouched: isTrue(value), 234 | }; 235 | }; 236 | } 237 | 238 | export class GetDropdownField { 239 | static getDropdownDefaultField = ( 240 | value = "", 241 | id = `${Utils.createUUID()}` 242 | ) => { 243 | return { 244 | id: id, 245 | type: FORM.inputDropdown, 246 | title: FieldTitle.phone, 247 | name: Contact.phone, 248 | value: value, 249 | required: true, 250 | validation: { 251 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.phone), 252 | [ValidationType.isPhone]: Utils.getErrorNotValidForm(FieldTitle.phone), 253 | }, 254 | isValid: isTrue(value), 255 | isTouched: isTrue(value), 256 | }; 257 | }; 258 | 259 | static getDropdownEmailField = (value = "", id = `${Utils.createUUID()}`) => { 260 | return { 261 | id: id, 262 | type: FORM.inputDropdown, 263 | title: FieldTitle.email, 264 | name: Contact.email, 265 | value: value, 266 | required: true, 267 | validation: { 268 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.email), 269 | [ValidationType.isEmail]: Utils.getErrorNotValidForm(FieldTitle.email), 270 | }, 271 | isValid: isTrue(value), 272 | isTouched: isTrue(value), 273 | }; 274 | }; 275 | 276 | static getDropdownPhoneField = (value = "", id = `${Utils.createUUID()}`) => { 277 | return { 278 | id: id, 279 | type: FORM.inputDropdown, 280 | title: FieldTitle.phone, 281 | name: Contact.phone, 282 | value: value, 283 | required: true, 284 | validation: { 285 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.phone), 286 | [ValidationType.isPhone]: Utils.getErrorNotValidForm(FieldTitle.phone), 287 | }, 288 | isValid: isTrue(value), 289 | isTouched: isTrue(value), 290 | }; 291 | }; 292 | 293 | static getDropdownAdditionalPhoneField = ( 294 | value = "", 295 | id = `${Utils.createUUID()}` 296 | ) => { 297 | return { 298 | id: id, 299 | type: FORM.inputDropdown, 300 | title: FieldTitle.additionalPhone, 301 | name: Contact.additionalPhone, 302 | value: value, 303 | required: true, 304 | validation: { 305 | [ValidationType.notEmpty]: Utils.getErrorNotFiled( 306 | FieldTitle.additionalPhone 307 | ), 308 | [ValidationType.isPhone]: Utils.getErrorNotValidForm( 309 | FieldTitle.additionalPhone 310 | ), 311 | }, 312 | isValid: isTrue(value), 313 | isTouched: isTrue(value), 314 | }; 315 | }; 316 | 317 | static getDropdownVkField = (value = "", id = `${Utils.createUUID()}`) => { 318 | return { 319 | id: id, 320 | type: FORM.inputDropdown, 321 | title: FieldTitle.vk, 322 | name: Contact.vk, 323 | value: value, 324 | required: true, 325 | validation: { 326 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.vk), 327 | }, 328 | isValid: isTrue(value), 329 | isTouched: isTrue(value), 330 | }; 331 | }; 332 | 333 | static getDropdownFbField = (value = "", id = `${Utils.createUUID()}`) => { 334 | return { 335 | id: id, 336 | type: FORM.inputDropdown, 337 | title: FieldTitle.fb, 338 | name: Contact.fb, 339 | value: value, 340 | required: true, 341 | validation: { 342 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.fb), 343 | }, 344 | isValid: isTrue(value), 345 | isTouched: isTrue(value), 346 | }; 347 | }; 348 | 349 | static getDropdownOtherField = (value = "", id = `${Utils.createUUID()}`) => { 350 | return { 351 | id: id, 352 | type: FORM.inputDropdown, 353 | title: FieldTitle.other, 354 | name: Contact.other, 355 | value: value, 356 | required: true, 357 | validation: { 358 | [ValidationType.notEmpty]: Utils.getErrorNotFiled(FieldTitle.other), 359 | }, 360 | isValid: isTrue(value), 361 | isTouched: isTrue(value), 362 | }; 363 | }; 364 | } 365 | 366 | export const LimitFieldCount = 13; 367 | 368 | export const FilterType = { 369 | ID: `id`, 370 | FIO: `fio`, 371 | }; 372 | 373 | export const InitiateFilters = () => { 374 | return [ 375 | { 376 | id: `${Utils.createUUID()}`, 377 | type: FilterType.ID, 378 | value: "", 379 | }, 380 | { 381 | id: `${Utils.createUUID()}`, 382 | type: FilterType.FIO, 383 | value: "", 384 | }, 385 | ]; 386 | }; 387 | 388 | export const MAX_PHONE_LENGTH = 11; 389 | -------------------------------------------------------------------------------- /client/scripts/main.js: -------------------------------------------------------------------------------- 1 | import AppView from "./views/app-view.js"; 2 | 3 | const appView = new AppView(); 4 | appView.init(); 5 | -------------------------------------------------------------------------------- /client/scripts/models/clients-model.js: -------------------------------------------------------------------------------- 1 | import Observer from "../utils/observer.js"; 2 | 3 | import { SortType } from "../const.js"; 4 | 5 | export default class ClientsModel extends Observer { 6 | constructor() { 7 | super(); 8 | 9 | this._clients = []; 10 | 11 | this._currentPage = 1; 12 | this._pages = 1; 13 | 14 | this._currentClient = null; 15 | 16 | this._currentSortType = SortType.ID_UP; 17 | 18 | this._currentModalMode = null; 19 | } 20 | 21 | getCurrentPage() { 22 | return this._currentPage; 23 | } 24 | 25 | getPagesCount() { 26 | return this._pages; 27 | } 28 | 29 | setIsClientsLoading(updateType, update) { 30 | this._notify(updateType, update); 31 | } 32 | 33 | getClient(id) { 34 | return this._clients[id]; 35 | } 36 | 37 | getClients() { 38 | return this._clients; 39 | } 40 | 41 | getClientsCount() { 42 | return this._clients.length; 43 | } 44 | 45 | setClients(updateType, { clients, page, pages }) { 46 | this._currentPage = page; 47 | this._pages = pages; 48 | this._clients = clients.slice(); 49 | 50 | this._notify(updateType, { isLoading: false }); 51 | } 52 | 53 | setSortType(updateType, sortType) { 54 | this._currentSortType = sortType; 55 | this._notify(updateType, { isLoading: false }); 56 | } 57 | 58 | setCurrentModalMode(updateType, currentModalMode) { 59 | this._currentModalMode = currentModalMode; 60 | this._notify(updateType, { isLoading: false }); 61 | } 62 | 63 | getCurrentModalMode() { 64 | return this._currentModalMode; 65 | } 66 | 67 | getCurrentSortType() { 68 | return this._currentSortType; 69 | } 70 | 71 | setCurrentClient(response) { 72 | this._currentClient = response; 73 | } 74 | 75 | getCurrentClient() { 76 | return this._currentClient; 77 | } 78 | 79 | resetCurrentModalMode(updateType) { 80 | this._currentModalMode = null; 81 | this._notify(updateType); 82 | } 83 | 84 | resetCurrentClient() { 85 | this._currentClient = null; 86 | } 87 | 88 | updateClient(updateType, update) { 89 | const index = this._clients.findIndex( 90 | (client) => client._id === update._id 91 | ); 92 | 93 | if (index === -1) return; 94 | 95 | this._clients = [ 96 | ...this._clients.slice(0, index), 97 | update, 98 | ...this._clients.slice(index + 1), 99 | ]; 100 | 101 | this._notify(updateType, update); 102 | } 103 | 104 | deleteClient(updateType, update) { 105 | const index = this._clients.findIndex((client) => client._id === update); 106 | 107 | if (index === -1) return; 108 | 109 | this._clients = [ 110 | ...this._clients.slice(0, index), 111 | ...this._clients.slice(index + 1), 112 | ]; 113 | 114 | this._notify(updateType, update); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /client/scripts/models/filter-model.js: -------------------------------------------------------------------------------- 1 | import Observer from "../utils/observer.js"; 2 | import { InitiateFilters } from "../const.js"; 3 | 4 | export default class FilterModel extends Observer { 5 | constructor() { 6 | super(); 7 | 8 | this._filterFields = InitiateFilters(); 9 | } 10 | 11 | resetFilters(updateType) { 12 | this._filterFields = InitiateFilters(); 13 | this._notify(updateType); 14 | } 15 | 16 | getFilter(type) { 17 | return this._filterFields.find((item) => item.type === type); 18 | } 19 | 20 | getFilters() { 21 | return this._filterFields; 22 | } 23 | 24 | updateFilter(updateType, update) { 25 | const index = this._filterFields.findIndex( 26 | (field) => field.type === update.type 27 | ); 28 | 29 | if (index === -1) { 30 | throw new Error(`Can't update nonexistent filter`); 31 | } 32 | 33 | this._filterFields = [ 34 | ...this._filterFields.slice(0, index), 35 | update, 36 | ...this._filterFields.slice(index + 1), 37 | ]; 38 | 39 | this._notify(updateType, update); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/scripts/models/form-model.js: -------------------------------------------------------------------------------- 1 | import Observer from "../utils/observer.js"; 2 | import { InitiateFields } from "../const.js"; 3 | 4 | export default class FormModel extends Observer { 5 | constructor() { 6 | super(); 7 | 8 | this._formFields = InitiateFields(); 9 | this._errorMessages = []; 10 | } 11 | 12 | mapFieldsForServer() { 13 | const data = {}; 14 | 15 | this.getFields().forEach( 16 | (field) => (data[field.id] = { name: field.name, value: field.value }) 17 | ); 18 | 19 | return data; 20 | } 21 | 22 | resetFields() { 23 | this._formFields = InitiateFields(); 24 | } 25 | 26 | getField(id) { 27 | return this._formFields.find((item) => item.id === id); 28 | } 29 | 30 | getFields() { 31 | return this._formFields; 32 | } 33 | 34 | setFields(newFields) { 35 | return (this._formFields = newFields); 36 | } 37 | 38 | getFieldsCount() { 39 | return this._formFields.length; 40 | } 41 | 42 | getErrorMessages() { 43 | return this._errorMessages; 44 | } 45 | 46 | setError(error) { 47 | if (this._errorMessages.includes(error)) return; 48 | 49 | this._errorMessages.push(error); 50 | } 51 | 52 | deleteError(newError) { 53 | this._errorMessages = this._errorMessages.filter( 54 | (error) => newError !== error 55 | ); 56 | } 57 | 58 | resetError() { 59 | this._errorMessages = []; 60 | } 61 | 62 | setField(updateType, newField) { 63 | this._formFields = this._formFields.slice().concat([newField]); 64 | 65 | this._notify(updateType); 66 | } 67 | 68 | updateField(updateType, update) { 69 | const index = this._formFields.findIndex((field) => field.id === update.id); 70 | 71 | if (index === -1) { 72 | throw new Error(`Can't update nonexistent field`); 73 | } 74 | 75 | this._formFields = [ 76 | ...this._formFields.slice(0, index), 77 | update, 78 | ...this._formFields.slice(index + 1), 79 | ]; 80 | 81 | this._notify(updateType, update); 82 | } 83 | 84 | deleteField(updateType, updateId) { 85 | const index = this._formFields.findIndex((field) => field.id === updateId); 86 | if (index === -1) { 87 | throw new Error(`Can't delete nonexistent field`); 88 | } 89 | 90 | this._formFields = [ 91 | ...this._formFields.slice(0, index), 92 | ...this._formFields.slice(index + 1), 93 | ]; 94 | 95 | this._notify(updateType, updateId); 96 | } 97 | 98 | isFormValid() { 99 | return this._formFields.some((field) => !field.isValid); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/scripts/models/search-model.js: -------------------------------------------------------------------------------- 1 | import Observer from "../utils/observer.js"; 2 | 3 | export default class SearchModel extends Observer { 4 | constructor() { 5 | super(); 6 | 7 | this._currentKeyword = ""; 8 | this._clients = []; 9 | } 10 | 11 | getClients() { 12 | return this._clients; 13 | } 14 | 15 | getClientsCount() { 16 | return this._clients.length; 17 | } 18 | 19 | setClients(updateType, { clients, page, pages }) { 20 | this._currentPage = page; 21 | this._pages = pages; 22 | this._clients = clients.slice(); 23 | 24 | this._notify(updateType, { isLoading: false }); 25 | } 26 | 27 | resetClients(updateType) { 28 | this._clients = []; 29 | 30 | this._notify(updateType, { isLoading: false }); 31 | } 32 | 33 | getCurrentKeyword() { 34 | return this._currentKeyword; 35 | } 36 | 37 | setCurrentKeyword(keyword) { 38 | this._currentKeyword = keyword; 39 | } 40 | 41 | resetCurrentKeyword() { 42 | this._currentKeyword = ""; 43 | } 44 | 45 | updateClient(updateType, update) { 46 | const index = this._clients.findIndex( 47 | (client) => client._id === update._id 48 | ); 49 | 50 | if (index === -1) return; 51 | 52 | this._clients = [ 53 | ...this._clients.slice(0, index), 54 | update, 55 | ...this._clients.slice(index + 1), 56 | ]; 57 | } 58 | 59 | deleteClient(updateType, update) { 60 | const index = this._clients.findIndex((client) => client._id === update); 61 | 62 | if (index === -1) return; 63 | 64 | this._clients = [ 65 | ...this._clients.slice(0, index), 66 | ...this._clients.slice(index + 1), 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/scripts/presenters/clients-presenter.js: -------------------------------------------------------------------------------- 1 | import { END_POINT, UpdateType, UserAction } from "../const.js"; 2 | 3 | import Router from "../utils/router.js"; 4 | 5 | import Api from "../api/api.js"; 6 | 7 | import ClientsModel from "../models/clients-model.js"; 8 | import FilterModel from "../models/filter-model.js"; 9 | import FormModel from "../models/form-model.js"; 10 | import SearchModel from "../models/search-model.js"; 11 | 12 | import Utils from "../utils/common.js"; 13 | 14 | import { 15 | SortType, 16 | FilterType, 17 | MODE, 18 | FieldTitle, 19 | GetDropdownField, 20 | GetInputField, 21 | State, 22 | Contact, 23 | FORM, 24 | } from "../const.js"; 25 | 26 | import { validationMethods, formatInput } from "../utils/validate.js"; 27 | 28 | export default class ClientsPresenter { 29 | constructor(view) { 30 | this._view = view; 31 | 32 | this._api = new Api(END_POINT); 33 | 34 | this._router = new Router(this); 35 | 36 | this._clientsModel = new ClientsModel(); 37 | this._filterModel = new FilterModel(); 38 | this._formModel = new FormModel(); 39 | this._searchModel = new SearchModel(); 40 | 41 | this._clientsModel.addObserver(this._handleModelEvent); 42 | this._filterModel.addObserver(this._handleModelEvent); 43 | this._formModel.addObserver(this._handleModelEvent); 44 | this._searchModel.addObserver(this._handleModelEvent); 45 | } 46 | 47 | // Handlers 48 | _handleModelEvent = (updateType, data) => { 49 | switch (updateType) { 50 | case UpdateType.PATCH: 51 | this._view.updatePatch(data); 52 | break; 53 | 54 | case UpdateType.MINOR: 55 | this._view.updateMinor(data); 56 | break; 57 | case UpdateType.MINOR_FORM: 58 | this._view.updateMinorForm(data); 59 | break; 60 | 61 | case UpdateType.MAJOR: 62 | this._view.updateMajor(); 63 | break; 64 | } 65 | }; 66 | 67 | handlePaginationChange = (pageMode, currentPage) => { 68 | if (pageMode === "prev") { 69 | this._router.navigateTo(`/page/${currentPage - 1}`); 70 | } else if (pageMode === "next") { 71 | this._router.navigateTo(`/page/${currentPage + 1}`); 72 | } 73 | }; 74 | 75 | handleSortTypeChange = (sortType) => { 76 | const currentSortType = this.getCurrentSortType(); 77 | if (currentSortType === sortType) { 78 | return; 79 | } 80 | 81 | this._clientsModel.setSortType(UpdateType.MINOR, sortType); 82 | }; 83 | 84 | handleFilterTypeChange = (filterType, value) => { 85 | value = value.toLowerCase(); 86 | 87 | const filter = this._filterModel.getFilter(filterType); 88 | 89 | if (!filter) { 90 | throw new Error(`Can't find nonexistent filter: ${filter}`); 91 | } 92 | 93 | filter.value = value; 94 | 95 | this._filterModel.updateFilter(UpdateType.MINOR, filter); 96 | }; 97 | 98 | handleFilterTypeReset = () => { 99 | this._filterModel.resetFilters(UpdateType.MINOR); 100 | }; 101 | 102 | handleProfileClientClick = (clientId) => { 103 | this._router.navigateTo(`/client/${clientId}`); 104 | }; 105 | 106 | handleAddButtonClick = () => { 107 | const currentModalMode = MODE.addClient; 108 | this._clientsModel.setCurrentModalMode(UpdateType.MINOR, currentModalMode); 109 | }; 110 | 111 | handleEditButtonClick = (clientId) => { 112 | const currentModalMode = MODE.editClient; 113 | this._fetchClient(clientId, currentModalMode); 114 | }; 115 | 116 | handleDeleteButtonClick = (clientId) => { 117 | const currentModalMode = MODE.deleteClient; 118 | 119 | this._fetchClient(clientId, currentModalMode); 120 | }; 121 | 122 | handleDeleteClientClick = (clientId) => { 123 | this._handleViewAction( 124 | UserAction.DELETE_CLIENT, 125 | UpdateType.MINOR, 126 | clientId 127 | ); 128 | }; 129 | 130 | handleCloseModal = () => { 131 | const currentPage = this.getCurrentPage(); 132 | this._router.navigateTo(`/page/${currentPage}`); 133 | 134 | this._clientsModel.resetCurrentClient(); 135 | this._formModel.resetFields(); 136 | this._formModel.resetError(); 137 | this._clientsModel.resetCurrentModalMode(UpdateType.MINOR); 138 | }; 139 | 140 | escKeyDownHandler = (evt) => { 141 | Utils.addEscapeEvent(evt, this.handleCloseModal); 142 | }; 143 | 144 | handleDropdownChange = (fieldTitle, fieldId) => { 145 | let newField; 146 | switch (fieldTitle) { 147 | case FieldTitle.email: 148 | newField = GetDropdownField.getDropdownEmailField(); 149 | break; 150 | case FieldTitle.phone: 151 | newField = GetDropdownField.getDropdownPhoneField(); 152 | break; 153 | case FieldTitle.additionalPhone: 154 | newField = GetDropdownField.getDropdownAdditionalPhoneField(); 155 | break; 156 | case FieldTitle.vk: 157 | newField = GetDropdownField.getDropdownVkField(); 158 | break; 159 | case FieldTitle.fb: 160 | newField = GetDropdownField.getDropdownFbField(); 161 | break; 162 | case FieldTitle.other: 163 | newField = GetDropdownField.getDropdownOtherField(); 164 | break; 165 | default: 166 | break; 167 | } 168 | 169 | if (!newField) { 170 | throw new Error(`Can't create nonexistent field: ${fieldTitle}`); 171 | } 172 | 173 | newField.id = fieldId; 174 | this._formModel.updateField(UpdateType.PATCH, newField); 175 | }; 176 | 177 | _validateField(field) { 178 | if (!field.required) return true; 179 | 180 | if (!field) { 181 | throw new Error(`Can't find nonexistent field: ${field}`); 182 | } 183 | 184 | const validationField = field.validation; 185 | 186 | for (const method in validationField) { 187 | const errorMessage = field.validation[method]; 188 | 189 | if ( 190 | validationField.hasOwnProperty(method) && 191 | !validationMethods(method, field) && 192 | !field.deletedField 193 | ) { 194 | this._formModel.setError(errorMessage); 195 | return false; 196 | } 197 | 198 | this._formModel.deleteError(errorMessage); 199 | } 200 | 201 | return true; 202 | } 203 | 204 | handleFieldChange = (fieldId, value) => { 205 | const field = this._formModel.getField(fieldId); 206 | 207 | if (!field) { 208 | throw new Error(`Can't find nonexistent field: ${field}`); 209 | } 210 | 211 | if ( 212 | [FieldTitle.name, FieldTitle.lastname, FieldTitle.surname].includes( 213 | field.title 214 | ) 215 | ) { 216 | value = formatInput(value); 217 | } 218 | 219 | field.value = value; 220 | field.isTouched = true; 221 | 222 | const isValid = this._validateField(field); 223 | field.isValid = isValid; 224 | 225 | this._formModel.updateField(UpdateType.PATCH, field); 226 | }; 227 | 228 | _handleViewAction(actionType, updateType, client) { 229 | switch (actionType) { 230 | case UserAction.ADD_CLIENT: 231 | this._view.saveButtonComponent.setViewState(State.ADDING); 232 | this._api 233 | .addClient(client) 234 | .then(() => { 235 | this._api.getClients().then((response) => { 236 | this._clientsModel.setClients(updateType, response); 237 | }); 238 | }) 239 | .catch((e) => { 240 | console.error(e); 241 | Utils.toast(`${e.message}`); 242 | }) 243 | .finally(() => { 244 | this._view.saveButtonComponent.setViewState(State.ABORTING); 245 | this.handleCloseModal(); 246 | }); 247 | break; 248 | 249 | case UserAction.EDIT_CLIENT: 250 | this._view.editButtonComponent.setViewState(State.ADDING); 251 | this._api 252 | .updateClient(client) 253 | .then((client) => { 254 | this._searchModel.updateClient(updateType, client); 255 | this._clientsModel.updateClient(updateType, client); 256 | }) 257 | .catch((e) => { 258 | console.error(e); 259 | Utils.toast(`${e.message}`); 260 | }) 261 | .finally(() => { 262 | this._view.editButtonComponent.setViewState(State.ABORTING); 263 | this.handleCloseModal(); 264 | }); 265 | break; 266 | 267 | case UserAction.DELETE_CLIENT: 268 | if (this._view.deleteClientModalComponent) { 269 | this._view.deleteClientModalComponent.setViewState(State.ADDING); 270 | } 271 | if (this._cancelButtonComponent) { 272 | this._cancelButtonComponent.setViewState(State.ADDING); 273 | } 274 | 275 | this._api 276 | .deleteClient(client._id) 277 | .then(() => { 278 | this._searchModel.deleteClient(null, client._id); 279 | this._clientsModel.deleteClient(UpdateType.MINOR, client._id); 280 | }) 281 | .catch((e) => { 282 | console.error(e); 283 | Utils.toast(`${e.message}`); 284 | }) 285 | .finally(() => { 286 | if (this._view.deleteClientModalComponent) { 287 | this._view.deleteClientModalComponent.setViewState( 288 | State.ABORTING 289 | ); 290 | } 291 | if (this._cancelButtonComponent) { 292 | this._cancelButtonComponent.setViewState(State.ABORTING); 293 | } 294 | this.handleCloseModal(); 295 | }); 296 | break; 297 | } 298 | } 299 | 300 | handleAddClientClick = () => { 301 | const newClient = this._formModel.mapFieldsForServer(); 302 | 303 | this._handleViewAction(UserAction.ADD_CLIENT, UpdateType.MINOR, newClient); 304 | }; 305 | 306 | handleEditClientClick = (client) => { 307 | let updateClient = this._formModel.mapFieldsForServer(); 308 | updateClient = Object.assign({}, updateClient, { 309 | [client._id]: { name: "_id", value: client._id }, 310 | }); 311 | 312 | this._handleViewAction( 313 | UserAction.EDIT_CLIENT, 314 | UpdateType.MINOR, 315 | updateClient 316 | ); 317 | }; 318 | 319 | handleAddContactClick = () => { 320 | const newField = GetDropdownField.getDropdownDefaultField(); 321 | this._formModel.setField(UpdateType.MINOR_FORM, newField); 322 | }; 323 | 324 | handleDeleteContactClick = (buttonId) => { 325 | let field; 326 | if (buttonId) { 327 | field = this._formModel.getField(buttonId); 328 | } 329 | 330 | if (!field) { 331 | throw new Error(`Can't find nonexistent field: ${field}`); 332 | } 333 | 334 | field.deletedField = true; 335 | 336 | this._validateField(field); 337 | 338 | this._formModel.deleteField(UpdateType.MINOR_FORM, buttonId); 339 | }; 340 | 341 | handleSearchContactInput = (value) => { 342 | this._searchModel.setCurrentKeyword(value); 343 | 344 | if (value) { 345 | this._searchClient(value); 346 | } else { 347 | this._searchModel.resetClients(UpdateType.MINOR); 348 | } 349 | }; 350 | 351 | _searchClient(searchValue) { 352 | this._clientsModel.setIsClientsLoading(UpdateType.MINOR, { 353 | isLoading: true, 354 | }); 355 | 356 | this._api 357 | .searchClients(searchValue) 358 | .then((clients) => { 359 | this._searchModel.setClients(UpdateType.MINOR, clients); 360 | }) 361 | .catch((e) => { 362 | console.error(e); 363 | Utils.toast(`${e.message}`); 364 | this._searchModel.setClients(UpdateType.MINOR, []); 365 | }); 366 | } 367 | 368 | _parseClientToFormFileds(client) { 369 | let newFields = []; 370 | 371 | for (const prop in client) { 372 | if (!client.hasOwnProperty(prop)) return; 373 | 374 | if (typeof client[prop] === "string") { 375 | switch (prop) { 376 | case Contact.name: 377 | newFields.push(GetInputField.getInputNameField(client[prop])); 378 | break; 379 | case Contact.surname: 380 | newFields.push(GetInputField.getInputSurnameField(client[prop])); 381 | break; 382 | case Contact.lastname: 383 | newFields.push(GetInputField.getInputLastnameField(client[prop])); 384 | break; 385 | default: 386 | break; 387 | } 388 | } 389 | 390 | if (Array.isArray(client[prop])) { 391 | for (const { type, value } of client[prop]) { 392 | switch (type) { 393 | case Contact.email: 394 | newFields.push(GetDropdownField.getDropdownEmailField(value)); 395 | break; 396 | case Contact.phone: 397 | newFields.push(GetDropdownField.getDropdownPhoneField(value)); 398 | break; 399 | case Contact.additionalPhone: 400 | newFields.push( 401 | GetDropdownField.getDropdownAdditionalPhoneField(value) 402 | ); 403 | break; 404 | case Contact.vk: 405 | newFields.push(GetDropdownField.getDropdownVkField(value)); 406 | break; 407 | case Contact.fb: 408 | newFields.push(GetDropdownField.getDropdownFbField(value)); 409 | break; 410 | case Contact.other: 411 | newFields.push(GetDropdownField.getDropdownOtherField(value)); 412 | break; 413 | default: 414 | break; 415 | } 416 | } 417 | } 418 | } 419 | 420 | newFields = newFields.sort((prevField, field) => { 421 | if ( 422 | prevField.type === FORM.inputText && 423 | field.type === FORM.inputDropdown 424 | ) { 425 | return -1; 426 | } 427 | 428 | return 1; 429 | }); 430 | this._formModel.setFields(newFields); 431 | } 432 | 433 | // Api 434 | _fetchClient(clientId, currentModalMode) { 435 | this._clientsModel.setIsClientsLoading(UpdateType.MINOR, { 436 | isLoading: true, 437 | }); 438 | this._api 439 | .getClient(clientId) 440 | .then((client) => { 441 | this._clientsModel.setCurrentClient(client); 442 | 443 | if (currentModalMode === MODE.editClient) { 444 | this._parseClientToFormFileds(client); 445 | } 446 | 447 | this._clientsModel.setCurrentModalMode( 448 | UpdateType.MINOR, 449 | currentModalMode 450 | ); 451 | }) 452 | .catch((e) => { 453 | console.error(e.message); 454 | Utils.toast(e.message); 455 | this._clientsModel.setCurrentClient(UpdateType.MINOR, {}); 456 | }); 457 | } 458 | 459 | _fetchClients(page) { 460 | this._clientsModel.setIsClientsLoading(UpdateType.MINOR, { 461 | isLoading: true, 462 | }); 463 | this._api 464 | .getClients(page) 465 | .then((response) => { 466 | this._clientsModel.setClients(UpdateType.MINOR, response); 467 | }) 468 | .catch((e) => { 469 | console.error(e.message); 470 | Utils.toast(e.message); 471 | this._clientsModel.setClients(UpdateType.MINOR, { 472 | clients: [], 473 | page: 1, 474 | pages: 1, 475 | }); 476 | }); 477 | } 478 | 479 | // Routes 480 | pageRoute(currentPage = 1) { 481 | if ( 482 | this.getCurrentPage() === +currentPage && 483 | this.getClientsCount() !== 0 484 | ) { 485 | return; 486 | } 487 | this._fetchClients(currentPage); 488 | } 489 | 490 | clientIdRoute(id) { 491 | const currentModalMode = MODE.profile; 492 | this._fetchClient(id, currentModalMode); 493 | } 494 | 495 | _filterClients(clients) { 496 | const filters = this.getFilters(); 497 | 498 | for (const { type, value } of filters) { 499 | if (value) { 500 | clients = clients.filter((client) => { 501 | if (type === FilterType.ID) { 502 | return client._id.includes(value); 503 | } 504 | 505 | if (type === FilterType.FIO) { 506 | const name = client.name ? client.name : ""; 507 | const surname = client.surname ? client.surname : ""; 508 | const lastname = client.lastname ? client.lastname : ""; 509 | return `${surname} ${name} ${lastname}`.includes(value); 510 | } 511 | 512 | if (type === FilterType.BIRTH) { 513 | return Utils.formatDate(client.dateOfBirth).includes(value); 514 | } 515 | 516 | if (type === FilterType.LEARN) { 517 | return Utils.formatLearnYear(client.dateStartLearn).includes(value); 518 | } 519 | 520 | if (type === FilterType.FACULTY) { 521 | return client.faculty.includes(value); 522 | } 523 | 524 | return true; 525 | }); 526 | } 527 | } 528 | 529 | return clients; 530 | } 531 | 532 | _sortClients(clients, currentSortType) { 533 | switch (currentSortType) { 534 | case SortType.ID_UP: 535 | return clients.sort(Utils.sortClientsByIdUp); 536 | case SortType.ID_DOWN: 537 | return clients.sort(Utils.sortClientsByIdDown); 538 | case SortType.NAME_UP: 539 | return clients.sort(Utils.sortClientsByNameUp); 540 | case SortType.NAME_DOWN: 541 | return clients.sort(Utils.sortClientsByNameDown); 542 | case SortType.DATE_CREATE_UP: 543 | return clients.sort(Utils.sortClientsByCreateDateUp); 544 | case SortType.DATE_CREATE_DOWN: 545 | return clients.sort(Utils.sortClientsByCreateDateDown); 546 | case SortType.DATE_UPDATE_UP: 547 | return clients.sort(Utils.sortClientsByUpdateDateUp); 548 | case SortType.DATE_UPDATE_DOWN: 549 | return clients.sort(Utils.sortClientsByUpdateDateDown); 550 | default: 551 | return clients; 552 | } 553 | } 554 | 555 | // Model 556 | getClients() { 557 | const clients = this._clientsModel.getClients(); 558 | 559 | const filteredClients = this._filterClients(clients); 560 | 561 | const currentSortType = this.getCurrentSortType(); 562 | 563 | return this._sortClients(filteredClients, currentSortType); 564 | } 565 | 566 | getClientsCount() { 567 | return this._clientsModel.getClientsCount(); 568 | } 569 | 570 | getCurrentPage() { 571 | return this._clientsModel.getCurrentPage(); 572 | } 573 | 574 | getClientsCount() { 575 | return this._clientsModel.getClientsCount(); 576 | } 577 | 578 | getPagesCount() { 579 | return this._clientsModel.getPagesCount(); 580 | } 581 | 582 | getCurrentClient() { 583 | return this._clientsModel.getCurrentClient(); 584 | } 585 | 586 | resetCurrentClient() { 587 | return this._clientsModel.resetCurrentClient(); 588 | } 589 | 590 | getCurrentSortType = () => { 591 | return this._clientsModel.getCurrentSortType(); 592 | }; 593 | 594 | getCurrentModalMode() { 595 | return this._clientsModel.getCurrentModalMode(); 596 | } 597 | 598 | resetCurrentModalMode() { 599 | return this._clientsModel.resetCurrentModalMode(); 600 | } 601 | 602 | getFilters() { 603 | return this._filterModel.getFilters(); 604 | } 605 | 606 | getFormField(fieldId) { 607 | return this._formModel.getField(fieldId); 608 | } 609 | 610 | getFormFields() { 611 | return this._formModel.getFields(); 612 | } 613 | 614 | getFormFieldsCount() { 615 | return this._formModel.getFieldsCount(); 616 | } 617 | 618 | resetFormFields() { 619 | return this._formModel.resetFields(); 620 | } 621 | 622 | getErrorMessages() { 623 | return this._formModel.getErrorMessages(); 624 | } 625 | 626 | getCurrentKeyword() { 627 | return this._searchModel.getCurrentKeyword(); 628 | } 629 | 630 | getSearchClientsCount() { 631 | return this._searchModel.getClientsCount(); 632 | } 633 | 634 | getSearchClients() { 635 | return this._searchModel.getClients(); 636 | } 637 | 638 | init() { 639 | this._router.init(); 640 | } 641 | } 642 | -------------------------------------------------------------------------------- /client/scripts/utils/common.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | static addEscapeEvent(evt, action) { 3 | const isEscKey = evt.key === `Escape` || evt.key === `Esc`; 4 | 5 | if (isEscKey) { 6 | action(evt); 7 | } 8 | } 9 | 10 | static getShortDescription(description) { 11 | return description.length >= DESCRIPTION_LENGTH 12 | ? `${description.slice(0, DESCRIPTION_LENGTH - 1)}...` 13 | : description; 14 | } 15 | 16 | static isOnline() { 17 | return window.navigator.onLine; 18 | } 19 | 20 | static sortClientsByIdUp(clientA, clientB) { 21 | return clientA._id > clientB._id ? 1 : -1; 22 | } 23 | 24 | static sortClientsByIdDown(clientA, clientB) { 25 | return clientA._id < clientB._id ? 1 : -1; 26 | } 27 | 28 | static sortClientsByNameUp(clientA, clientB) { 29 | return `${clientA.surname} ${clientA.name} ${clientA.lastname}` > 30 | `${clientB.surname} ${clientB.name} ${clientB.lastname}` 31 | ? 1 32 | : -1; 33 | } 34 | 35 | static sortClientsByNameDown(clientA, clientB) { 36 | return `${clientA.surname} ${clientA.name} ${clientA.lastname}` < 37 | `${clientB.surname} ${clientB.name} ${clientB.lastname}` 38 | ? 1 39 | : -1; 40 | } 41 | 42 | static sortClientsByCreateDateUp(clientA, clientB) { 43 | return clientA.createdAt > clientB.createdAt ? 1 : -1; 44 | } 45 | 46 | static sortClientsByCreateDateDown(clientA, clientB) { 47 | return clientA.createdAt < clientB.createdAt ? 1 : -1; 48 | } 49 | 50 | static sortClientsByUpdateDateUp(clientA, clientB) { 51 | return clientA.updatedAt > clientB.updatedAt ? 1 : -1; 52 | } 53 | 54 | static sortClientsByUpdateDateDown(clientA, clientB) { 55 | return clientA.updatedAt < clientB.updatedAt ? 1 : -1; 56 | } 57 | 58 | static toast(message) { 59 | const SHOW_TIME = 5000; 60 | const toastContainer = document.createElement(`div`); 61 | const toastItem = document.createElement(`div`); 62 | toastContainer.classList.add(`toast-container`); 63 | 64 | document.body.append(toastContainer); 65 | 66 | toastItem.textContent = message; 67 | toastItem.classList.add(`toast-item`); 68 | 69 | toastContainer.append(toastItem); 70 | 71 | setTimeout(() => { 72 | toastItem.remove(); 73 | }, SHOW_TIME); 74 | } 75 | 76 | static toUpperCaseFirstLetter(string) { 77 | return string.charAt(0).toUpperCase() + string.slice(1); 78 | } 79 | 80 | static formatDate(date) { 81 | return new Date(date).toLocaleDateString("ru-RU", { 82 | year: "numeric", 83 | month: "2-digit", 84 | day: "2-digit", 85 | }); 86 | } 87 | 88 | static formatTime(date) { 89 | return new Date(date).toLocaleTimeString("ru-RU", { 90 | hour: "2-digit", 91 | minute: "2-digit", 92 | }); 93 | } 94 | 95 | static getErrorNotFiled(title) { 96 | return `Ошибка: Поле "${title}" должно быть заполнено`; 97 | } 98 | 99 | static getErrorNotValidForm(title) { 100 | return `Ошибка: Невалидная форма ${title}`; 101 | } 102 | 103 | static createUUID() { 104 | let dt = new Date().getTime(); 105 | const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( 106 | /[xy]/g, 107 | function (c) { 108 | const r = (dt + Math.random() * 16) % 16 | 0; 109 | dt = Math.floor(dt / 16); 110 | return (c == "x" ? r : (r & 0x3) | 0x8).toString(16); 111 | } 112 | ); 113 | return uuid; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /client/scripts/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, evt, ms, timer) { 2 | if (timer.timerId) { 3 | clearTimeout(timer.timerId); 4 | } 5 | 6 | timer.timerId = setTimeout(() => { 7 | func(evt); 8 | }, ms); 9 | } 10 | -------------------------------------------------------------------------------- /client/scripts/utils/observer.js: -------------------------------------------------------------------------------- 1 | export default class Observer { 2 | constructor() { 3 | this._observers = new Set(); 4 | } 5 | 6 | _notify(event, payload) { 7 | this._observers.forEach((observer) => observer(event, payload)); 8 | } 9 | 10 | addObserver(observer) { 11 | this._observers.add(observer); 12 | } 13 | 14 | removeObserver(observer) { 15 | this._observers.delete(observer); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/scripts/utils/render.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "../views/abstract-view.js"; 2 | import { RenderPosition } from "../const.js"; 3 | 4 | export default class Render { 5 | static createElement(template) { 6 | const element = document.createElement(`div`); 7 | 8 | element.innerHTML = template; 9 | 10 | return element.firstElementChild; 11 | } 12 | 13 | static remove(component) { 14 | if (component === null) { 15 | return; 16 | } 17 | 18 | if (!(component instanceof AbstractView)) { 19 | throw new Error(`Can remove only components`); 20 | } 21 | 22 | component.getElement().remove(); 23 | component.removeElement(); 24 | } 25 | 26 | static render(container, element, position = RenderPosition.BEFOREEND) { 27 | if (container instanceof AbstractView) { 28 | container = container.getElement(); 29 | } 30 | 31 | let $element; 32 | if (element instanceof AbstractView) { 33 | $element = element.getElement(); 34 | } 35 | 36 | switch (position) { 37 | case RenderPosition.AFTERBEGIN: 38 | container.prepend($element); 39 | break; 40 | case RenderPosition.BEFOREEND: 41 | container.append($element); 42 | break; 43 | case RenderPosition.BEFOREBEGIN: 44 | container.before($element); 45 | break; 46 | case RenderPosition.AFTEREND: 47 | container.after($element); 48 | break; 49 | } 50 | } 51 | 52 | static renderTemplate( 53 | container, 54 | template, 55 | position = RenderPosition.BEFOREEND 56 | ) { 57 | if (container instanceof AbstractView) { 58 | container = container.getElement(); 59 | } 60 | 61 | return container.insertAdjacentHTML(position, template); 62 | } 63 | 64 | static replace(newElement, oldElement) { 65 | if (newElement instanceof AbstractView) { 66 | newElement = newElement.getElement(); 67 | } 68 | 69 | if (oldElement instanceof AbstractView) { 70 | oldElement = oldElement.getElement(); 71 | } 72 | 73 | const parentElement = oldElement.parentElement; 74 | 75 | if (parentElement === null || oldElement === null || newElement === null) { 76 | throw new Error(`Can't replace unexisting elements`); 77 | } 78 | 79 | if (parentElement.contains(oldElement)) { 80 | parentElement.replaceChild(newElement, oldElement); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/scripts/utils/router.js: -------------------------------------------------------------------------------- 1 | export default class Router { 2 | constructor(presenter) { 3 | this._presenter = presenter; 4 | } 5 | 6 | navigateTo = (url) => { 7 | if (url === location.pathname) return; 8 | 9 | history.pushState(null, null, url); 10 | this._router(); 11 | }; 12 | 13 | _isValidParam = (arrPathname) => { 14 | return arrPathname.length === 2 && arrPathname[1]; 15 | }; 16 | 17 | _router = () => { 18 | const arrPathname = location.pathname.split(`/`).slice(1); 19 | 20 | if (arrPathname[0] === `page` && this._isValidParam(arrPathname)) { 21 | this._presenter.pageRoute(arrPathname[1]); 22 | return; 23 | } 24 | 25 | if (arrPathname[0] === `client` && this._isValidParam(arrPathname)) { 26 | this._presenter.clientIdRoute(arrPathname[1]); 27 | return; 28 | } 29 | 30 | this.navigateTo(`/page/1`); 31 | }; 32 | 33 | init() { 34 | window.addEventListener("popstate", this._router); 35 | this._router(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/scripts/utils/validate.js: -------------------------------------------------------------------------------- 1 | import { ValidationType, MAX_PHONE_LENGTH } from "../const.js"; 2 | 3 | const isNotEmpty = ({ value }) => { 4 | return !!value.trim(); 5 | }; 6 | 7 | const isPhone = ({ value }) => { 8 | const isCodeValid = value.indexOf(`+7`) === 0; 9 | const isCorrect = value.replace(/[-\+()]/g, ``).length === MAX_PHONE_LENGTH; 10 | 11 | return value && isCodeValid && isCorrect; 12 | }; 13 | 14 | const isEmail = ({ value }) => { 15 | const re = 16 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 17 | 18 | return re.test(value); 19 | }; 20 | 21 | export const validationMethods = (method, field) => { 22 | switch (method) { 23 | case ValidationType.notEmpty: 24 | return isNotEmpty(field); 25 | case ValidationType.isPhone: 26 | return isPhone(field); 27 | case ValidationType.isEmail: 28 | return isEmail(field); 29 | default: 30 | break; 31 | } 32 | }; 33 | 34 | const allowSymbols = /^[а-яА-ЯёЁa-zA-Z0-9 -]+$/g; 35 | 36 | export const formatInput = (field) => { 37 | field = field 38 | .split("") 39 | .filter((char) => char.match(allowSymbols) !== null) 40 | .join("") 41 | .trim() 42 | .toLowerCase() 43 | .replace(/^-+/g, "") 44 | .replace(/-+$/g, "") 45 | .replace(/ +(?= )/g, "") 46 | .replace(/-+/g, "-"); 47 | 48 | return field.charAt(0).toUpperCase() + field.slice(1); 49 | }; 50 | -------------------------------------------------------------------------------- /client/scripts/views/abstract-view.js: -------------------------------------------------------------------------------- 1 | import Render from "../utils/render.js"; 2 | 3 | export default class Abstract { 4 | constructor() { 5 | if (new.target === Abstract) { 6 | throw new Error(`Can't instantiate Abstract, only concrete one.`); 7 | } 8 | 9 | this._element = null; 10 | this._handler = {}; 11 | } 12 | 13 | getElement() { 14 | if (!this._element) { 15 | this._element = Render.createElement(this.getTemplate()); 16 | } 17 | 18 | return this._element; 19 | } 20 | 21 | getTemplate() { 22 | throw new Error(`Abstract method not implemented: getTemplate`); 23 | } 24 | 25 | removeElement() { 26 | this._element = null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/scripts/views/app-view.js: -------------------------------------------------------------------------------- 1 | import ClientsPresenter from "../presenters/clients-presenter.js"; 2 | 3 | import Render from "../utils/render.js"; 4 | 5 | import HeaderContainer from "./header-container.js"; 6 | import MainContainer from "./main-container.js"; 7 | 8 | import HeaderView from "./header-view.js"; 9 | 10 | import SearchProfileView from "./search-profile-view.js"; 11 | 12 | import PaginationView from "./pagination-view.js"; 13 | 14 | import LoadingView from "./loading-view.js"; 15 | 16 | import TableHeadView from "./table-head-view.js"; 17 | import FilteringListView from "./filtering-list-view.js"; 18 | import TableBodyView from "./table-body-view.js"; 19 | import ClientProfileView from "./client-profile-view.js"; 20 | import AddButtonView from "./button-add-client.js"; 21 | import AddButtonContactView from "./button-add-contact.js"; 22 | import ErrorsContainerView from "./errors-container.js"; 23 | import FormErrorsView from "./form-errors.js"; 24 | import FilledButtonView from "./button-filled.js"; 25 | import ButtonLinkView from "./button-link.js"; 26 | 27 | import ModalView from "./modal-view.js"; 28 | import ModalTitleView from "./modal-title.js"; 29 | import ModalPersonInfoView from "./modal-person-info.js"; 30 | 31 | import FormContainerView from "./form-container.js"; 32 | import InputFieldView from "./input-view.js"; 33 | import DropdownView from "./dropdown-view.js"; 34 | 35 | import NoClientsSearchView from "./no-search-clients-view.js"; 36 | import NoClientsView from "./no-clients-view.js"; 37 | 38 | import ModalDeleteClientView from "./modal-delete-client.js"; 39 | 40 | import { 41 | RenderPosition, 42 | MODE, 43 | ModalTitle, 44 | FORM, 45 | ButtonTitle, 46 | LimitFieldCount, 47 | } from "../const.js"; 48 | 49 | export default class AppView { 50 | constructor() { 51 | this._clientsPresenter = new ClientsPresenter(this); 52 | 53 | this._rootContainer = document.getElementById("root"); 54 | 55 | this._headerContainer = new HeaderContainer(); 56 | this._mainContainer = new MainContainer(); 57 | 58 | this._headerComponent = new HeaderView(); 59 | this._headerComponent.setInputHandler( 60 | this._clientsPresenter.handleSearchContactInput 61 | ); 62 | this._searchContainer = this._headerComponent.getSearchContainer(); 63 | 64 | this._tableBodyContainer = new TableBodyView(); 65 | 66 | this._noClientsComponent = new NoClientsView(); 67 | 68 | this._clientsComponents = new Map(); 69 | this._clientsSearchComponents = new Map(); 70 | this._formFieldComponents = new Map(); 71 | 72 | this._currentModalMode = null; 73 | 74 | this.isLoading = true; 75 | } 76 | 77 | _renderLoading() { 78 | this._loadingComponent = new LoadingView(); 79 | Render.render(this._mainTableContainer, this._loadingComponent); 80 | } 81 | 82 | _renderPagination() { 83 | const currentPage = this._clientsPresenter.getCurrentPage(); 84 | const pages = this._clientsPresenter.getPagesCount(); 85 | 86 | this._paginationComponent = new PaginationView(currentPage, pages); 87 | 88 | this._paginationComponent.setPaginationChangeHandler( 89 | this._clientsPresenter.handlePaginationChange 90 | ); 91 | 92 | Render.render( 93 | this._mainTableContainer, 94 | this._paginationComponent, 95 | RenderPosition.AFTERBEGIN 96 | ); 97 | } 98 | 99 | _renderFiltration() { 100 | const filters = this._clientsPresenter.getFilters(); 101 | this._filteringListComponent = new FilteringListView(filters); 102 | 103 | this._filteringListComponent.setFilterTypeChangeHandler( 104 | this._clientsPresenter.handleFilterTypeChange 105 | ); 106 | 107 | this._filteringListComponent.setFilterTypeResetHandler( 108 | this._clientsPresenter.handleFilterTypeReset 109 | ); 110 | 111 | Render.render(this._mainTableContainer, this._filteringListComponent); 112 | } 113 | 114 | _createClientComponent(client) { 115 | const clientComponent = new ClientProfileView(client); 116 | clientComponent.setProfileClientClickHandler( 117 | this._clientsPresenter.handleProfileClientClick 118 | ); 119 | clientComponent.setEditClientClickHandler( 120 | this._clientsPresenter.handleEditButtonClick 121 | ); 122 | clientComponent.setDeleteClientClickHandler( 123 | this._clientsPresenter.handleDeleteButtonClick 124 | ); 125 | 126 | return clientComponent; 127 | } 128 | 129 | _renderClient(container, components, client) { 130 | const clientComponent = this._createClientComponent(client); 131 | Render.render(container, clientComponent); 132 | components.set(client._id, clientComponent); 133 | } 134 | 135 | _renderNoClients() { 136 | Render.render(this._mainContainer, this._noClientsComponent); 137 | } 138 | 139 | _clearClientsTable() { 140 | if (this._tableHeadComponent) { 141 | Render.remove(this._tableHeadComponent); 142 | } 143 | 144 | this._clientsComponents.forEach((component) => Render.remove(component)); 145 | this._clientsComponents.clear(); 146 | } 147 | 148 | _renderClientsTable() { 149 | if (this._clientsPresenter.getClientsCount() === 0) { 150 | this._renderNoClients(); 151 | return; 152 | } 153 | 154 | const currentSortType = this._clientsPresenter.getCurrentSortType(); 155 | this._tableHeadComponent = new TableHeadView(currentSortType); 156 | 157 | this._tableHeadComponent.setSortTypeChangeHandler( 158 | this._clientsPresenter.handleSortTypeChange 159 | ); 160 | 161 | Render.render(this._mainTableContainer, this._tableHeadComponent); 162 | 163 | this._renderFiltration(); 164 | 165 | Render.render(this._mainTableContainer, this._tableBodyContainer); 166 | 167 | const clients = this._clientsPresenter.getClients(); 168 | 169 | clients.forEach((client) => 170 | this._renderClient( 171 | this._tableBodyContainer, 172 | this._clientsComponents, 173 | client 174 | ) 175 | ); 176 | } 177 | 178 | _renderAddButton() { 179 | if (this._addButtonComponent !== null) { 180 | this._addMoreButtonComponent = null; 181 | } 182 | 183 | this._addButtonComponent = new AddButtonView(); 184 | 185 | this._addButtonComponent.setClickHandler( 186 | this._clientsPresenter.handleAddButtonClick 187 | ); 188 | 189 | Render.render( 190 | this._mainContainer, 191 | this._addButtonComponent, 192 | RenderPosition.AFTEREND 193 | ); 194 | } 195 | 196 | _clearProfileClientForm() { 197 | if (this._modalTitleComponent) { 198 | Render.remove(this._modalTitleComponent); 199 | } 200 | 201 | if (this._formContainerComponent) { 202 | Render.remove(this._formContainerComponent); 203 | } 204 | } 205 | 206 | _renderProfileClientForm() { 207 | const currentClient = this._clientsPresenter.getCurrentClient(); 208 | 209 | if (!currentClient) return; 210 | 211 | const container = this._modalComponent.getModalContainer(); 212 | 213 | const modalTitle = ModalTitle.profile; 214 | this._modalTitleComponent = new ModalTitleView( 215 | modalTitle, 216 | currentClient._id 217 | ); 218 | Render.render(container, this._modalTitleComponent); 219 | 220 | this._formContainerComponent = new ModalPersonInfoView(currentClient); 221 | Render.render(container, this._formContainerComponent); 222 | } 223 | 224 | _createInput(field) { 225 | const inputComponent = new InputFieldView(field); 226 | 227 | inputComponent.setChangeHandler(this._clientsPresenter.handleFieldChange); 228 | 229 | this._formFieldComponents.set(field.id, inputComponent); 230 | 231 | return inputComponent; 232 | } 233 | 234 | _createDropdown(field) { 235 | const dropdownComponent = new DropdownView(field); 236 | 237 | dropdownComponent.setDeleteClickHandler( 238 | this._clientsPresenter.handleDeleteContactClick 239 | ); 240 | 241 | dropdownComponent.setChangeHandlerDropdown( 242 | this._clientsPresenter.handleDropdownChange 243 | ); 244 | 245 | dropdownComponent.setChangeHandlerInput( 246 | this._clientsPresenter.handleFieldChange 247 | ); 248 | 249 | this._formFieldComponents.set(field.id, dropdownComponent); 250 | return dropdownComponent; 251 | } 252 | 253 | _createFieldComponent(field) { 254 | switch (field.type) { 255 | case FORM.inputText: 256 | return this._createInput(field); 257 | case FORM.inputDropdown: 258 | return this._createDropdown(field); 259 | default: 260 | return null; 261 | } 262 | } 263 | 264 | _renderFormField(container, fieldComponent) { 265 | Render.render(container, fieldComponent); 266 | } 267 | 268 | _renerFormErrors() { 269 | const container = this._errorsContainer; 270 | 271 | const errors = this._clientsPresenter.getErrorMessages(); 272 | this._formErrorsComponent = new FormErrorsView(errors); 273 | 274 | Render.render(container, this._formErrorsComponent); 275 | } 276 | 277 | _renderSaveButton() { 278 | const container = this._errorsContainer.getErrorsContainer(); 279 | 280 | const isFormValid = this._clientsPresenter 281 | .getFormFields() 282 | .some((field) => field.required && !field.isValid); 283 | 284 | this.saveButtonComponent = new FilledButtonView( 285 | ButtonTitle.save, 286 | isFormValid 287 | ); 288 | 289 | this.saveButtonComponent.setClientClickHandler( 290 | this._clientsPresenter.handleAddClientClick 291 | ); 292 | 293 | Render.render(container, this.saveButtonComponent, RenderPosition.AFTEREND); 294 | } 295 | 296 | _clearFormFields() { 297 | this._formFieldComponents.forEach((component) => Render.remove(component)); 298 | this._formFieldComponents.clear(); 299 | } 300 | 301 | _clearAddClientForm() { 302 | if (this._modalTitleComponent) { 303 | Render.remove(this._modalTitleComponent); 304 | } 305 | 306 | if (this._formContainerComponent) { 307 | Render.remove(this._formContainerComponent); 308 | } 309 | 310 | if (this._errorsContainer) { 311 | Render.remove(this._errorsContainer); 312 | } 313 | 314 | if (this._addButtonContactComponent) { 315 | Render.remove(this._addButtonContactComponent); 316 | } 317 | 318 | if (this._formErrorsComponent) { 319 | Render.remove(this._formErrorsComponent); 320 | } 321 | 322 | if (this.saveButtonComponent) { 323 | Render.remove(this.saveButtonComponent); 324 | } 325 | 326 | if (this._cancelButtonComponent) { 327 | Render.remove(this._cancelButtonComponent); 328 | } 329 | } 330 | 331 | _renderAddClientForm() { 332 | const formFields = this._clientsPresenter.getFormFields(); 333 | const fieldComponents = formFields.map((field) => 334 | this._createFieldComponent(field) 335 | ); 336 | 337 | const container = this._modalComponent.getModalContainer(); 338 | 339 | const addClientTitle = ModalTitle.add; 340 | this._modalTitleComponent = new ModalTitleView(addClientTitle); 341 | Render.render(container, this._modalTitleComponent); 342 | 343 | this._formContainerComponent = new FormContainerView(); 344 | Render.render(container, this._formContainerComponent); 345 | fieldComponents.forEach((fieldComponent) => { 346 | this._renderFormField(this._formContainerComponent, fieldComponent); 347 | }); 348 | 349 | if (this._clientsPresenter.getFormFieldsCount() < LimitFieldCount) { 350 | this._addButtonContactComponent = new AddButtonContactView(); 351 | this._addButtonContactComponent.setClickHandler( 352 | this._clientsPresenter.handleAddContactClick 353 | ); 354 | 355 | Render.render(container, this._addButtonContactComponent); 356 | } 357 | 358 | this._errorsContainer = new ErrorsContainerView(); 359 | Render.render(container, this._errorsContainer); 360 | this._renerFormErrors(); 361 | 362 | this._renderSaveButton(); 363 | 364 | this._cancelButtonComponent = new ButtonLinkView(ButtonTitle.cancel); 365 | this._cancelButtonComponent.setClientClickHandler( 366 | this._clientsPresenter.handleCloseModal 367 | ); 368 | 369 | Render.render(container, this._cancelButtonComponent); 370 | } 371 | 372 | _renderEditButton() { 373 | const currentClient = this._clientsPresenter.getCurrentClient(); 374 | if (!currentClient) return; 375 | 376 | const container = this._errorsContainer.getErrorsContainer(); 377 | 378 | const isFormValid = this._clientsPresenter 379 | .getFormFields() 380 | .some((field) => field.required && !field.isValid); 381 | 382 | this.editButtonComponent = new FilledButtonView( 383 | ButtonTitle.save, 384 | isFormValid, 385 | currentClient 386 | ); 387 | 388 | this.editButtonComponent.setClientClickHandler( 389 | this._clientsPresenter.handleEditClientClick 390 | ); 391 | 392 | Render.render(container, this.editButtonComponent, RenderPosition.AFTEREND); 393 | } 394 | 395 | _renderEditClientForm() { 396 | const currentClient = this._clientsPresenter.getCurrentClient(); 397 | if (!currentClient) return; 398 | 399 | const formFields = this._clientsPresenter.getFormFields(); 400 | 401 | const fieldComponents = formFields.map((field) => 402 | this._createFieldComponent(field) 403 | ); 404 | 405 | const container = this._modalComponent.getModalContainer(); 406 | 407 | const editClientTitle = ModalTitle.edit; 408 | this._modalTitleComponent = new ModalTitleView( 409 | editClientTitle, 410 | currentClient._id 411 | ); 412 | Render.render(container, this._modalTitleComponent); 413 | 414 | this._formContainerComponent = new FormContainerView(); 415 | Render.render(container, this._formContainerComponent); 416 | fieldComponents.forEach((fieldComponent) => { 417 | this._renderFormField(this._formContainerComponent, fieldComponent); 418 | }); 419 | 420 | if (this._clientsPresenter.getFormFieldsCount() < LimitFieldCount) { 421 | this._addButtonContactComponent = new AddButtonContactView(); 422 | this._addButtonContactComponent.setClickHandler( 423 | this._clientsPresenter.handleAddContactClick 424 | ); 425 | 426 | Render.render(container, this._addButtonContactComponent); 427 | } 428 | 429 | this._errorsContainer = new ErrorsContainerView(); 430 | Render.render(container, this._errorsContainer); 431 | this._renerFormErrors(); 432 | 433 | this._renderEditButton(currentClient); 434 | 435 | this._cancelButtonComponent = new ButtonLinkView( 436 | ButtonTitle.delete, 437 | currentClient 438 | ); 439 | this._cancelButtonComponent.setClientClickHandler( 440 | this._clientsPresenter.handleDeleteClientClick 441 | ); 442 | 443 | Render.render(container, this._cancelButtonComponent); 444 | } 445 | 446 | _clearEditClientForm() { 447 | if (this._modalTitleComponent) { 448 | Render.remove(this._modalTitleComponent); 449 | } 450 | 451 | if (this._formContainerComponent) { 452 | Render.remove(this._formContainerComponent); 453 | } 454 | 455 | if (this._errorsContainer) { 456 | Render.remove(this._errorsContainer); 457 | } 458 | 459 | if (this._addButtonContactComponent) { 460 | Render.remove(this._addButtonContactComponent); 461 | } 462 | 463 | if (this._formErrorsComponent) { 464 | Render.remove(this._formErrorsComponent); 465 | } 466 | 467 | if (this.editButtonComponent) { 468 | Render.remove(this.editButtonComponent); 469 | } 470 | 471 | if (this._cancelButtonComponent) { 472 | Render.remove(this._cancelButtonComponent); 473 | } 474 | } 475 | 476 | _renderDeleteClientForm() { 477 | const currentClient = this._clientsPresenter.getCurrentClient(); 478 | if (!currentClient) return; 479 | 480 | const container = this._modalComponent.getModalContainer(); 481 | 482 | this.deleteClientModalComponent = new ModalDeleteClientView(currentClient); 483 | this.deleteClientModalComponent.setCancelClickHandler( 484 | this._clientsPresenter.handleCloseModal 485 | ); 486 | this.deleteClientModalComponent.setDeleteClickHandler( 487 | this._clientsPresenter.handleDeleteClientClick 488 | ); 489 | 490 | Render.render(container, this.deleteClientModalComponent); 491 | } 492 | 493 | _clearDeleteClientForm() { 494 | if (this.deleteClientModalComponent) { 495 | Render.remove(this.deleteClientModalComponent); 496 | } 497 | } 498 | 499 | _clearForm(currentModalMode) { 500 | this._clearFormFields(); 501 | switch (currentModalMode) { 502 | case MODE.profile: 503 | this._clearProfileClientForm(); 504 | return; 505 | case MODE.addClient: 506 | this._clearAddClientForm(); 507 | return; 508 | case MODE.editClient: 509 | this._clearEditClientForm(); 510 | return; 511 | case MODE.deleteClient: 512 | this._clearDeleteClientForm(); 513 | return; 514 | default: 515 | break; 516 | } 517 | } 518 | 519 | _renderForm(currentModalMode) { 520 | switch (currentModalMode) { 521 | case MODE.profile: 522 | this._renderProfileClientForm(); 523 | return; 524 | case MODE.addClient: 525 | this._renderAddClientForm(); 526 | return; 527 | case MODE.editClient: 528 | this._renderEditClientForm(); 529 | return; 530 | case MODE.deleteClient: 531 | this._renderDeleteClientForm(); 532 | return; 533 | 534 | default: 535 | break; 536 | } 537 | } 538 | 539 | _renderModal() { 540 | const currentModalMode = this._clientsPresenter.getCurrentModalMode(); 541 | if (!currentModalMode) return; 542 | 543 | this._modalComponent = new ModalView(); 544 | 545 | document.body.style.overflow = "hidden"; 546 | 547 | document.addEventListener( 548 | `keydown`, 549 | this._clientsPresenter.escKeyDownHandler 550 | ); 551 | 552 | this._modalComponent.setCloseModalClickHandler( 553 | this._clientsPresenter.handleCloseModal 554 | ); 555 | 556 | Render.render(document.body, this._modalComponent); 557 | 558 | this._renderForm(currentModalMode); 559 | } 560 | 561 | _clearModal() { 562 | const currentModalMode = this._clientsPresenter.getCurrentModalMode(); 563 | 564 | if (this._modalComponent) { 565 | Render.remove(this._modalComponent); 566 | } 567 | 568 | document.body.style.overflow = "auto"; 569 | document.removeEventListener(`keydown`, this._escKeyDownHandler); 570 | 571 | this._clearForm(currentModalMode); 572 | } 573 | 574 | _createSearchClientComponent(client) { 575 | const clientSearchComponent = new SearchProfileView(client); 576 | clientSearchComponent.setProfileClientClickHandler( 577 | this._clientsPresenter.handleProfileClientClick 578 | ); 579 | clientSearchComponent.setEditClientClickHandler( 580 | this._clientsPresenter.handleEditButtonClick 581 | ); 582 | clientSearchComponent.setDeleteClientClickHandler( 583 | this._clientsPresenter.handleDeleteButtonClick 584 | ); 585 | 586 | return clientSearchComponent; 587 | } 588 | 589 | _renderSearchClient(container, components, client) { 590 | const clientSearchComponent = this._createSearchClientComponent(client); 591 | Render.render(container, clientSearchComponent); 592 | components.set(client._id, clientSearchComponent); 593 | } 594 | 595 | _clearSearchClientsTable() { 596 | if (this._tableHeadComponent) { 597 | Render.remove(this._tableHeadComponent); 598 | } 599 | 600 | this._clientsSearchComponents.forEach((component) => 601 | Render.remove(component) 602 | ); 603 | this._clientsSearchComponents.clear(); 604 | } 605 | 606 | _renderNoSearchClients() { 607 | this._noClientsSearchComponent = new NoClientsSearchView(); 608 | Render.render(this._searchContainer, this._noClientsSearchComponent); 609 | } 610 | 611 | _renderSearchClientsTable() { 612 | if (this._clientsPresenter.getSearchClientsCount() === 0) { 613 | this._renderNoSearchClients(); 614 | return; 615 | } 616 | 617 | const clients = this._clientsPresenter.getSearchClients(); 618 | 619 | clients.forEach((client) => 620 | this._renderSearchClient( 621 | this._searchContainer, 622 | this._clientsSearchComponents, 623 | client 624 | ) 625 | ); 626 | } 627 | 628 | clearMinor() { 629 | if (this._loadingComponent) { 630 | Render.remove(this._loadingComponent); 631 | } 632 | 633 | if (this._paginationComponent) { 634 | Render.remove(this._paginationComponent); 635 | } 636 | 637 | if (this._noClientsSearchComponent) { 638 | Render.remove(this._noClientsSearchComponent); 639 | } 640 | 641 | if (this._noClientsComponent) { 642 | Render.remove(this._noClientsComponent); 643 | } 644 | 645 | if (this._addButtonComponent) { 646 | Render.remove(this._addButtonComponent); 647 | } 648 | 649 | if (this.editButtonComponent) { 650 | Render.remove(this.editButtonComponent); 651 | } 652 | 653 | this._clearSearchClientsTable(); 654 | this._clearClientsTable(); 655 | this._clearModal(); 656 | } 657 | 658 | renderMinor({ isLoading } = {}) { 659 | if (isLoading !== undefined) { 660 | this.isLoading = isLoading; 661 | } 662 | 663 | if (this.isLoading) { 664 | this._renderLoading(); 665 | return; 666 | } 667 | 668 | if (this._clientsPresenter.getCurrentKeyword()) { 669 | this._renderSearchClientsTable(); 670 | } 671 | 672 | this._renderPagination(); 673 | 674 | if (this._filteringListComponent) { 675 | Render.remove(this._filteringListComponent); 676 | } 677 | 678 | this._renderClientsTable(); 679 | this._renderAddButton(); 680 | this._renderModal(); 681 | } 682 | 683 | updateMinor(data) { 684 | this.clearMinor(); 685 | this.renderMinor(data); 686 | } 687 | 688 | clearMinorForm() { 689 | const currentModalMode = this._clientsPresenter.getCurrentModalMode(); 690 | 691 | this._clearForm(currentModalMode); 692 | } 693 | 694 | renderMinorForm() { 695 | const currentModalMode = this._clientsPresenter.getCurrentModalMode(); 696 | 697 | this._renderForm(currentModalMode); 698 | } 699 | 700 | updateMinorForm(data) { 701 | this.clearMinorForm(); 702 | this.renderMinorForm(data); 703 | } 704 | 705 | clearMajor() { 706 | Render.remove(this._headerComponent); 707 | Render.remove(this._mainContainer); 708 | 709 | this._mainTableContainer = null; 710 | } 711 | 712 | renderMajor() { 713 | Render.render(this._rootContainer, this._headerContainer); 714 | Render.render(this._headerContainer, this._headerComponent); 715 | 716 | Render.render(this._rootContainer, this._mainContainer); 717 | this._mainTableContainer = this._mainContainer.getMainTableContainer(); 718 | } 719 | 720 | updateMajor() { 721 | this.clearMajor(); 722 | this.renderMajor(); 723 | } 724 | 725 | _replaceFieldComponent(fieldComponents, fieldId) { 726 | const oldFieldComponent = fieldComponents.get(fieldId); 727 | const newFieldComponent = this._createFieldComponent( 728 | this._clientsPresenter.getFormField(fieldId) 729 | ); 730 | 731 | Render.replace(newFieldComponent, oldFieldComponent); 732 | Render.remove(oldFieldComponent); 733 | 734 | fieldComponents.delete(fieldId); 735 | fieldComponents.set(fieldId, newFieldComponent); 736 | } 737 | 738 | clearPatch() { 739 | if (this._formErrorsComponent) { 740 | Render.remove(this._formErrorsComponent); 741 | } 742 | 743 | if (this.saveButtonComponent) { 744 | Render.remove(this.saveButtonComponent); 745 | } 746 | 747 | if (this.editButtonComponent) { 748 | Render.remove(this.editButtonComponent); 749 | } 750 | } 751 | 752 | renderPatch() { 753 | this._renerFormErrors(); 754 | 755 | const currentModalMode = this._clientsPresenter.getCurrentModalMode(); 756 | 757 | if (currentModalMode === MODE.addClient) { 758 | this._renderSaveButton(); 759 | } else if (currentModalMode === MODE.editClient) { 760 | this._renderEditButton(); 761 | } 762 | } 763 | 764 | updatePatch(data) { 765 | if (this._formFieldComponents.has(data.id)) { 766 | this._replaceFieldComponent(this._formFieldComponents, data.id); 767 | } 768 | 769 | this.clearPatch(); 770 | this.renderPatch(data); 771 | } 772 | 773 | init() { 774 | this.renderMajor(); 775 | this._clientsPresenter.init(); 776 | } 777 | } 778 | -------------------------------------------------------------------------------- /client/scripts/views/button-add-client.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createAddButtonTemplate = () => { 4 | return ` 5 |
6 | 15 |
16 | `; 17 | }; 18 | 19 | export default class AddButtonClient extends AbstractView { 20 | constructor() { 21 | super(); 22 | } 23 | 24 | _clickHandler = (evt) => { 25 | evt.preventDefault(); 26 | 27 | this._handler.click(); 28 | }; 29 | 30 | getTemplate() { 31 | return createAddButtonTemplate(); 32 | } 33 | 34 | setClickHandler(handler) { 35 | this._handler.click = handler; 36 | 37 | this.getElement() 38 | .querySelector(".js-popup-button") 39 | .addEventListener(`click`, this._clickHandler); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/scripts/views/button-add-contact.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createAddButtonTemplate = () => { 4 | return ` 5 |
6 | 12 |
13 | `; 14 | }; 15 | 16 | export default class AddButtonContact extends AbstractView { 17 | constructor() { 18 | super(); 19 | } 20 | 21 | _clickHandler = (evt) => { 22 | evt.preventDefault(); 23 | 24 | this._handler.click(); 25 | }; 26 | 27 | getTemplate() { 28 | return createAddButtonTemplate(); 29 | } 30 | 31 | setClickHandler(handler) { 32 | this._handler.click = handler; 33 | 34 | this.getElement() 35 | .querySelector(".js-button-contact") 36 | .addEventListener(`click`, this._clickHandler); 37 | } 38 | 39 | getAddButtonContainer() { 40 | return this.getElement().querySelector(`.button-contact`).parentNode; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/scripts/views/button-filled.js: -------------------------------------------------------------------------------- 1 | import SmartView from "./smart.js"; 2 | import { State } from "../const.js"; 3 | 4 | const addDisabledProperty = (isDisabled) => { 5 | return isDisabled ? `disabled` : ``; 6 | }; 7 | 8 | const saveButtonTemplate = (title, { isAdding, isDisabled }) => { 9 | const editButtonText = isAdding 10 | ? `
${title}
` 11 | : `${title}`; 12 | 13 | return ` 14 |
15 | 20 |
21 | `; 22 | }; 23 | 24 | export default class FilledButton extends SmartView { 25 | constructor(title, isDisabled, client) { 26 | super(); 27 | 28 | this._title = title; 29 | this._state = { isAdding: false, isDisabled }; 30 | this._handler = {}; 31 | this._client = client; 32 | } 33 | 34 | _clientClickHandler = (evt) => { 35 | evt.preventDefault(); 36 | 37 | if (this._client) { 38 | this._handler.click(this._client); 39 | } else { 40 | this._handler.click(); 41 | } 42 | }; 43 | 44 | getTemplate() { 45 | return saveButtonTemplate(this._title, this._state); 46 | } 47 | 48 | restoreHandlers() { 49 | this.setClientClickHandler(this._handler.click); 50 | } 51 | 52 | setClientClickHandler(handler) { 53 | this._handler.click = handler; 54 | 55 | this.getElement() 56 | .querySelector(`.js-button`) 57 | .addEventListener(`click`, this._clientClickHandler); 58 | } 59 | 60 | setViewState(state) { 61 | const resetFormState = () => { 62 | this.updateData({ 63 | isAdding: false, 64 | }); 65 | }; 66 | 67 | switch (state) { 68 | case State.ADDING: 69 | this.updateData({ 70 | isDisabled: true, 71 | isAdding: true, 72 | }); 73 | break; 74 | case State.ABORTING: 75 | resetFormState(); 76 | break; 77 | } 78 | 79 | this.updateElement(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /client/scripts/views/button-link.js: -------------------------------------------------------------------------------- 1 | import SmartView from "./smart.js"; 2 | import { State } from "../const.js"; 3 | 4 | const addDisabledProperty = (isDisabled) => { 5 | return isDisabled ? `disabled` : ``; 6 | }; 7 | 8 | const linkButtonTemplate = (title, { isAdding, isDisabled }) => { 9 | const editButtonText = isAdding 10 | ? `
${title}
` 11 | : `${title}`; 12 | 13 | return ` 14 |
15 | 20 |
21 | `; 22 | }; 23 | 24 | export default class ButtonLink extends SmartView { 25 | constructor(title, client, isDisabled = false) { 26 | super(); 27 | this._title = title; 28 | this._state = { isAdding: false, isDisabled }; 29 | this._handler = {}; 30 | this._client = client; 31 | } 32 | 33 | _clientClickHandler = (evt) => { 34 | evt.preventDefault(); 35 | 36 | if (this._client) { 37 | this._handler.click(this._client); 38 | } else { 39 | this._handler.click(); 40 | } 41 | }; 42 | 43 | getTemplate() { 44 | return linkButtonTemplate(this._title, this._state); 45 | } 46 | 47 | restoreHandlers() { 48 | this.setClientClickHandler(this._handler.click); 49 | } 50 | 51 | setClientClickHandler(handler) { 52 | this._handler.click = handler; 53 | 54 | this.getElement() 55 | .querySelector(`.js-button-link`) 56 | .addEventListener(`click`, this._clientClickHandler); 57 | } 58 | 59 | setViewState(state) { 60 | const resetFormState = () => { 61 | this.updateData({ 62 | isAdding: false, 63 | }); 64 | }; 65 | 66 | switch (state) { 67 | case State.ADDING: 68 | this.updateData({ 69 | isDisabled: true, 70 | isAdding: true, 71 | }); 72 | break; 73 | case State.ABORTING: 74 | resetFormState(); 75 | break; 76 | } 77 | 78 | this.updateElement(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/scripts/views/client-profile-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import Utils from "../utils/common.js"; 3 | import { ContactImage, ContactURL, FieldTitle, Contact } from "../const.js"; 4 | 5 | const getContactImage = (contact) => { 6 | switch (contact.type) { 7 | case Contact.email: 8 | return ContactImage.MAIL; 9 | case Contact.fb: 10 | return ContactImage.FB; 11 | case Contact.vk: 12 | return ContactImage.VK; 13 | case Contact.phone: 14 | return ContactImage.PHONE; 15 | case Contact.additionalPhone: 16 | return ContactImage.PHONE; 17 | default: 18 | return ContactImage.USER; 19 | } 20 | }; 21 | 22 | const getContactURL = (contact) => { 23 | switch (contact.type) { 24 | case Contact.email: 25 | return `mailto:${contact.value}`; 26 | case Contact.fb: 27 | return `${ContactURL.FB}${contact.value}`; 28 | case Contact.vk: 29 | return `${ContactURL.VK}${contact.value}`; 30 | case Contact.phone: 31 | return `tel:${contact.value}`; 32 | case Contact.additionalPhone: 33 | return `tel:${contact.value}`; 34 | default: 35 | return `#`; 36 | } 37 | }; 38 | 39 | const createContactItemTemplate = (contact) => { 40 | if (!contact.value) return; 41 | 42 | return `
  • 43 | 46 | ${getContactImage(contact)} 47 | 53 | ${FieldTitle[contact.type]}: ${ 54 | contact.value 55 | } 56 | 57 | 58 |
  • `; 59 | }; 60 | 61 | const createProfileItemTemplate = (client) => `
    62 |
    ${client._id.slice(-6)}
    63 |
    64 | 65 | ${Utils.toUpperCaseFirstLetter( 66 | client.surname 67 | )} ${Utils.toUpperCaseFirstLetter( 68 | client.name 69 | )} ${Utils.toUpperCaseFirstLetter(client.lastname)} 70 | 71 |
    72 |
    73 | ${Utils.formatDate( 74 | client.createdAt 75 | )}
    76 | ${Utils.formatTime(client.createdAt)} 77 |
    78 |
    79 | ${Utils.formatDate( 80 | client.updatedAt 81 | )}
    82 | ${Utils.formatTime(client.updatedAt)} 83 |
    84 |
    85 | 92 |
    93 |
    94 | 95 | 97 | 98 | 101 | 102 | 103 | Изменить 104 |
    105 | 106 | 108 | 109 | 112 | 113 | 114 | Удалить 115 | 116 |
    117 |
    `; 118 | 119 | export default class ClientProfile extends AbstractView { 120 | constructor(client) { 121 | super(); 122 | this._client = client; 123 | } 124 | 125 | getTemplate() { 126 | return createProfileItemTemplate(this._client); 127 | } 128 | 129 | _clickProfileHandler = (evt) => { 130 | if (!evt.target.classList.contains(`js-client-profile`)) { 131 | return; 132 | } 133 | 134 | evt.preventDefault(); 135 | this._handler.clickProfile(this._client._id); 136 | }; 137 | 138 | setProfileClientClickHandler(handler) { 139 | this._handler.clickProfile = handler; 140 | 141 | this.getElement().addEventListener(`click`, this._clickProfileHandler); 142 | } 143 | 144 | _clickEditHandler = (evt) => { 145 | if (!evt.target.classList.contains(`js-client-edit`)) { 146 | return; 147 | } 148 | 149 | evt.preventDefault(); 150 | this._handler.clickEdit(this._client._id); 151 | }; 152 | 153 | setEditClientClickHandler(handler) { 154 | this._handler.clickEdit = handler; 155 | 156 | this.getElement().addEventListener(`click`, this._clickEditHandler); 157 | } 158 | 159 | _clickDeleteHandler = (evt) => { 160 | if (!evt.target.classList.contains(`js-client-delete`)) { 161 | return; 162 | } 163 | 164 | evt.preventDefault(); 165 | this._handler.clickDelete(this._client._id); 166 | }; 167 | 168 | setDeleteClientClickHandler(handler) { 169 | this._handler.clickDelete = handler; 170 | 171 | this.getElement().addEventListener(`click`, this._clickDeleteHandler); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /client/scripts/views/clients-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createFilmsSectionTemplate = () => { 4 | return ` 5 |
    6 | `; 7 | }; 8 | 9 | export default class Films extends AbstractView { 10 | getTemplate() { 11 | return createFilmsSectionTemplate(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/scripts/views/dropdown-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import { Contact } from "../const.js"; 3 | 4 | const isValidTemplate = (isTouched, isValid) => { 5 | return isTouched && !isValid ? `form__input--error` : ``; 6 | }; 7 | 8 | export default class Dropdown extends AbstractView { 9 | constructor(field) { 10 | super(); 11 | 12 | this._field = field; 13 | this._id = field.id; 14 | 15 | this.init(); 16 | } 17 | 18 | getTemplate() { 19 | return `
    20 | 47 | 53 | 56 |
    `; 57 | } 58 | 59 | _clickHandler = (evt) => { 60 | evt.preventDefault(); 61 | if (!evt.target.classList.contains("js-delete-contact")) return; 62 | 63 | evt.stopPropagation(); 64 | 65 | this._handler.click(evt.target.dataset.id); 66 | }; 67 | 68 | setDeleteClickHandler(handler) { 69 | this._handler.click = handler; 70 | 71 | this.getElement() 72 | .querySelector(".js-delete-contact") 73 | .addEventListener(`click`, this._clickHandler); 74 | } 75 | 76 | _changeHandlerDropdown = (evt) => { 77 | evt.preventDefault(); 78 | 79 | this._handler.changeDropdown(evt.target.innerText, this._id); 80 | }; 81 | 82 | _setValid(evt) { 83 | evt.target.classList.remove(`form__input--error`); 84 | } 85 | 86 | setChangeHandlerDropdown = (handler) => { 87 | this._handler.changeDropdown = handler; 88 | }; 89 | 90 | _changeHandlerInput = (evt) => { 91 | evt.preventDefault(); 92 | 93 | if (!evt.target.classList.contains("js-input-contact")) return; 94 | 95 | this._handler.changeInput(evt.target.dataset.idinput, evt.target.value); 96 | }; 97 | 98 | setChangeHandlerInput = (handler) => { 99 | this._handler.changeInput = handler; 100 | 101 | this.getElement() 102 | .querySelector(".js-input-contact") 103 | .addEventListener(`focusout`, this._changeHandlerInput); 104 | 105 | this.getElement() 106 | .querySelector(".js-input-contact") 107 | .addEventListener(`focus`, (evt) => this._setValid(evt)); 108 | }; 109 | 110 | _closeDropdown = () => { 111 | // remove the open and active class from other opened Dropdown (Closing the opend DropDown) 112 | this.getElement() 113 | .querySelectorAll(".dropdown-container") 114 | .forEach((container) => { 115 | container.classList.remove("dropdown-open"); 116 | }); 117 | 118 | this.getElement() 119 | .querySelectorAll(".dropdown-menu") 120 | .forEach((menu) => { 121 | menu.classList.remove("dropdown-active"); 122 | }); 123 | }; 124 | 125 | _preventScroll(e) { 126 | if (e.keyCode === 38 || e.keyCode === 40) { 127 | e.preventDefault(); 128 | } 129 | } 130 | 131 | _keyNavigation(e) { 132 | const previousLink = 133 | e.target.parentElement.previousElementSibling?.firstElementChild; 134 | const nextLink = 135 | e.target.parentElement.nextElementSibling?.firstElementChild; 136 | 137 | if (e.keyCode === 38 && previousLink) { 138 | previousLink.focus(); 139 | } else if (e.keyCode === 40 && nextLink) { 140 | nextLink.focus(); 141 | } 142 | } 143 | 144 | _dropDownFunc = (dropDown) => { 145 | const innerText = (e) => { 146 | if (e.target.classList.contains("dropdown-item")) { 147 | dropDown.innerText = e.target.innerText; 148 | 149 | this._changeHandlerDropdown(e); 150 | } 151 | }; 152 | 153 | if (dropDown.classList.contains("click-dropdown")) { 154 | dropDown.addEventListener("click", (e) => { 155 | e.preventDefault(); 156 | 157 | if (dropDown.nextElementSibling.classList.contains("dropdown-active")) { 158 | // Close the clicked dropdown 159 | dropDown.parentElement.classList.remove("dropdown-open"); 160 | dropDown.nextElementSibling.classList.remove("dropdown-active"); 161 | dropDown.nextElementSibling.removeEventListener("click", innerText); 162 | } else { 163 | // Close the opend dropdown 164 | this._closeDropdown(); 165 | 166 | // add the open and active class(Opening the DropDown) 167 | dropDown.parentElement.classList.add("dropdown-open"); 168 | dropDown.nextElementSibling.classList.add("dropdown-active"); 169 | 170 | dropDown.nextElementSibling.addEventListener("click", innerText); 171 | 172 | dropDown.nextElementSibling.addEventListener( 173 | "keydown", 174 | this._keyNavigation 175 | ); 176 | 177 | dropDown.nextElementSibling.querySelector(".dropdown-item").focus(); 178 | } 179 | }); 180 | } 181 | }; 182 | 183 | _isDefaultContact = (contactField) => { 184 | return contactField.innerText === "Контакт"; 185 | }; 186 | 187 | init() { 188 | // Get all the dropdown from current dropdown 189 | this.getElement() 190 | .querySelectorAll(".dropdown-toggle") 191 | .forEach((dropDown) => this._dropDownFunc(dropDown)); 192 | 193 | // Listen to the doc click 194 | window.addEventListener("click", (e) => { 195 | // Close the menu if click happen outside menu 196 | if (e.target.closest(".dropdown-container") === null) { 197 | // Close the opend dropdown 198 | this._closeDropdown(); 199 | } 200 | }); 201 | window.addEventListener("keydown", this._preventScroll); 202 | 203 | const inputContact = this.getElement().querySelector(".js-input-contact"); 204 | const contactField = this.getElement().querySelector(".js-contact-field"); 205 | 206 | if (this._isDefaultContact(contactField)) { 207 | inputContact.disabled = true; 208 | } else { 209 | inputContact.disabled = false; 210 | } 211 | 212 | if ( 213 | this._field.name === Contact.phone || 214 | this._field.name === Contact.additionalPhone 215 | ) { 216 | this.formated = IMask(inputContact, { 217 | mask: `+{7}(000)000-00-00`, 218 | }); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /client/scripts/views/errors-container.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createErrorsContainerTemplate = () => { 4 | return ` 5 |
    6 | `; 7 | }; 8 | 9 | export default class ErrorsContainer extends AbstractView { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | getTemplate() { 15 | return createErrorsContainerTemplate(); 16 | } 17 | 18 | getErrorsContainer() { 19 | return document.querySelector(`.errors-container`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/scripts/views/filtering-list-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import { FilterType } from "../const.js"; 3 | import { debounce } from "../utils/debounce.js"; 4 | 5 | const getFilterValue = (filters, filterType) => { 6 | for (const filter of filters) { 7 | if (filter.type === filterType) { 8 | return filter.value; 9 | } 10 | } 11 | }; 12 | 13 | const createFilteringListTemplate = (filters) => { 14 | return ` 15 |
    16 |
    17 | 23 | 29 | 30 |
    31 |
    32 | `; 33 | }; 34 | 35 | export default class FilteringList extends AbstractView { 36 | constructor(filters = []) { 37 | super(); 38 | 39 | this._filters = filters; 40 | this.timer = { timerId: null }; 41 | } 42 | 43 | _filterTypeChangeHandler = (evt) => { 44 | evt.preventDefault(); 45 | 46 | const eventFilterType = evt.target.dataset.filterType; 47 | const eventFilterValue = evt.target.value; 48 | 49 | this._handler.filterTypeChange(eventFilterType, eventFilterValue); 50 | }; 51 | 52 | _resetHandler = (evt) => { 53 | if (evt.target.tagName !== `BUTTON`) { 54 | return; 55 | } 56 | 57 | evt.preventDefault(); 58 | 59 | this._handler.click(); 60 | }; 61 | 62 | getTemplate() { 63 | return createFilteringListTemplate(this._filters); 64 | } 65 | 66 | setFilterTypeChangeHandler(handler) { 67 | this._handler.filterTypeChange = handler; 68 | 69 | this.getElement().addEventListener(`input`, (evt) => 70 | debounce(this._filterTypeChangeHandler, evt, 1000, this.timer) 71 | ); 72 | } 73 | 74 | setFilterTypeResetHandler(handler) { 75 | this._handler.click = handler; 76 | 77 | this.getElement().addEventListener(`click`, this._resetHandler); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client/scripts/views/form-container.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const formContainerTemplate = () => { 4 | return `
    `; 5 | }; 6 | 7 | export default class FormContainer extends AbstractView { 8 | getTemplate() { 9 | return formContainerTemplate(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/scripts/views/form-errors.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createFormErrorTemplate = (error) => { 4 | return ` 5 |

    6 | ${error} 7 |

    8 | `; 9 | }; 10 | 11 | const createFormErrorsTemplate = (errors) => { 12 | const errorsGroup = errors 13 | .map((error) => { 14 | return createFormErrorTemplate(error); 15 | }) 16 | .join(``); 17 | 18 | return ` 19 |
    20 | ${errorsGroup} 21 |
    22 | `; 23 | }; 24 | 25 | export default class FormErrors extends AbstractView { 26 | constructor(errors) { 27 | super(); 28 | 29 | this._errors = errors; 30 | } 31 | 32 | getTemplate() { 33 | return createFormErrorsTemplate(this._errors); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/scripts/views/header-container.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createHeaderContainerTemplate = () => { 4 | return ` 5 |
    6 | `; 7 | }; 8 | 9 | export default class HeaderContainer extends AbstractView { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | getTemplate() { 15 | return createHeaderContainerTemplate(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/scripts/views/header-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import { debounce } from "../utils/debounce.js"; 3 | 4 | const createHeaderTemplate = () => `
    5 | 6 |
    7 |
    8 | 9 |
    10 | 11 |
    12 |
    `; 13 | export default class HeaderView extends AbstractView { 14 | constructor() { 15 | super(); 16 | 17 | this.timer = { timerId: null }; 18 | } 19 | 20 | _inputChangeHandler = (evt) => { 21 | if (!evt.target.classList.contains(`js-search-input`)) { 22 | return; 23 | } 24 | 25 | evt.preventDefault(); 26 | this._handler.input(evt.target.value); 27 | }; 28 | 29 | setInputHandler(handler) { 30 | this._handler.input = handler; 31 | 32 | this.getElement().addEventListener(`input`, (evt) => 33 | debounce(this._inputChangeHandler, evt, 1000, this.timer) 34 | ); 35 | } 36 | 37 | getHeaderContainer() { 38 | return this.getElement().querySelector(`.header__inner`); 39 | } 40 | 41 | getSearchContainer() { 42 | return this.getElement().querySelector(`.header__search`); 43 | } 44 | 45 | getTemplate() { 46 | return createHeaderTemplate(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/scripts/views/input-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const isRequiredTemplate = () => { 4 | return `*`; 5 | }; 6 | 7 | const isValidTemplate = (isTouched, isValid) => { 8 | return isTouched && !isValid ? `form__input--error` : ``; 9 | }; 10 | 11 | const isFocusedTemplate = (value) => { 12 | return value.trim() ? `form__label--focused` : ``; 13 | }; 14 | 15 | const inputFieldTemplate = (field) => { 16 | return ` 17 |
    18 | 24 | 27 |
    28 | `; 29 | }; 30 | 31 | export default class InputField extends AbstractView { 32 | constructor(field) { 33 | super(); 34 | 35 | this._field = field; 36 | this._changeHandler = this._changeHandler.bind(this); 37 | } 38 | 39 | _changeHandler(evt) { 40 | evt.preventDefault(); 41 | 42 | this._handler.change(evt.target.dataset.fieldid, evt.target.value); 43 | } 44 | 45 | getTemplate() { 46 | return inputFieldTemplate(this._field); 47 | } 48 | 49 | _setValid(evt) { 50 | evt.target.classList.remove(`form__input--error`); 51 | } 52 | 53 | setChangeHandler(handler) { 54 | this._handler.change = handler; 55 | 56 | this.getElement() 57 | .querySelector(".js-input") 58 | .addEventListener(`focusout`, this._changeHandler); 59 | 60 | this.getElement() 61 | .querySelector(".js-input") 62 | .addEventListener(`focus`, (evt) => this._setValid(evt)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/scripts/views/loading-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createLoadingTemplate = () => { 4 | return ` 5 |
    6 |
    7 | 58 |
    59 |
    60 |
    61 |
    62 |
    63 | `; 64 | }; 65 | 66 | export default class Loading extends AbstractView { 67 | getTemplate() { 68 | return createLoadingTemplate(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/scripts/views/main-container.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createMainContainerTemplate = () => { 4 | return `
    5 |
    6 |

    Клиенты

    7 |
    8 |
    9 | `; 10 | }; 11 | 12 | export default class MainContainer extends AbstractView { 13 | constructor() { 14 | super(); 15 | } 16 | 17 | getTemplate() { 18 | return createMainContainerTemplate(); 19 | } 20 | 21 | getMainTableContainer() { 22 | return document.querySelector(`.table`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/scripts/views/modal-delete-client.js: -------------------------------------------------------------------------------- 1 | import SmartView from "./smart.js"; 2 | import { State } from "../const.js"; 3 | 4 | const addDisabledProperty = (isDisabled) => { 5 | return isDisabled ? `disabled` : ``; 6 | }; 7 | 8 | const modalDeleteTemplate = ({ isAdding, isDisabled }) => { 9 | const editButtonText = isAdding 10 | ? `
    Удалить
    ` 11 | : `Удалить`; 12 | return ` 13 | 31 | 32 | `; 33 | }; 34 | 35 | export default class ModalDeleteClient extends SmartView { 36 | constructor(client) { 37 | super(); 38 | this._client = client; 39 | this._state = { isAdding: false, isDisabled: false }; 40 | this._handler = {}; 41 | } 42 | 43 | _clientDeleteClickHandler = (evt) => { 44 | evt.preventDefault(); 45 | 46 | this._handler.clickDelete(this._client); 47 | }; 48 | 49 | _clientCancelClickHandler = (evt) => { 50 | evt.preventDefault(); 51 | 52 | this._handler.clickCancel(this._client); 53 | }; 54 | 55 | getTemplate() { 56 | return modalDeleteTemplate(this._state); 57 | } 58 | 59 | restoreHandlers() { 60 | this.setDeleteClickHandler(this._handler.clickDelete); 61 | this.setCancelClickHandler(this._handler.clickCancel); 62 | } 63 | 64 | setDeleteClickHandler(handler) { 65 | this._handler.clickDelete = handler; 66 | 67 | this.getElement() 68 | .querySelector(`.js-button`) 69 | .addEventListener(`click`, this._clientDeleteClickHandler); 70 | } 71 | 72 | setCancelClickHandler(handler) { 73 | this._handler.clickCancel = handler; 74 | 75 | this.getElement() 76 | .querySelector(`.js-button-link`) 77 | .addEventListener(`click`, this._clientCancelClickHandler); 78 | } 79 | 80 | setViewState(state) { 81 | const resetFormState = () => { 82 | this.updateData({ 83 | isAdding: false, 84 | }); 85 | }; 86 | 87 | switch (state) { 88 | case State.ADDING: 89 | this.updateData({ 90 | isDisabled: false, 91 | isAdding: true, 92 | }); 93 | break; 94 | case State.ABORTING: 95 | resetFormState(); 96 | break; 97 | } 98 | 99 | this.updateElement(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/scripts/views/modal-person-info.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import Utils from "../utils/common.js"; 3 | import { ContactURL, Contact, FieldTitle } from "../const.js"; 4 | 5 | const getContactURL = (contact) => { 6 | switch (contact.type) { 7 | case Contact.email: 8 | return `mailto:${contact.value}`; 9 | case Contact.fb: 10 | return `${ContactURL.FB}${contact.value}`; 11 | case Contact.vk: 12 | return `${ContactURL.VK}${contact.value}`; 13 | case Contact.phone: 14 | return `tel:${contact.value}`; 15 | case Contact.additionalPhone: 16 | return `tel:${contact.value}`; 17 | default: 18 | return `#`; 19 | } 20 | }; 21 | 22 | const modalPersonInfoTemplate = (client) => { 23 | const contactsGroup = client.contacts 24 | .map((contact) => { 25 | if (!contact.value) return ``; 26 | 27 | return `
  • ${ 28 | FieldTitle[contact.type] 29 | }: 30 | 31 | ${contact.value} 32 | 33 |
  • `; 34 | }) 35 | .join(``); 36 | 37 | return `
    38 |

    Фамилия: ${Utils.toUpperCaseFirstLetter( 39 | client.surname 40 | )}

    41 |

    Имя: ${Utils.toUpperCaseFirstLetter( 42 | client.name 43 | )}

    44 |

    Отчество: ${ 45 | client.lastname ? Utils.toUpperCaseFirstLetter(client.lastname) : "" 46 | }

    47 |

    Контакты клиента

    48 | 51 |
    `; 52 | }; 53 | 54 | export default class ModalPersonInfo extends AbstractView { 55 | constructor(client) { 56 | super(); 57 | this._client = client; 58 | } 59 | 60 | getTemplate() { 61 | return modalPersonInfoTemplate(this._client); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/scripts/views/modal-title.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const getIdTemplate = (id) => { 4 | return `ID: ${id}`; 5 | }; 6 | 7 | const modalTitleTemplate = (title, id) => { 8 | return ``; 11 | }; 12 | 13 | export default class ModalTitle extends AbstractView { 14 | constructor(title, id = null) { 15 | super(); 16 | 17 | this._title = title; 18 | this._id = id; 19 | } 20 | 21 | getTemplate() { 22 | return modalTitleTemplate(this._title, this._id); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/scripts/views/modal-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createModalTemplate = () => ``; 9 | 10 | export default class ModalView extends AbstractView { 11 | constructor() { 12 | super(); 13 | this._handler = {}; 14 | } 15 | 16 | getTemplate() { 17 | return createModalTemplate(); 18 | } 19 | 20 | getModalContainer() { 21 | return this.getElement().querySelector(`.popup__inner`); 22 | } 23 | 24 | _closeModalClickHandler = (evt) => { 25 | if ( 26 | evt.target.classList.contains("js-modal-wrapper") || 27 | evt.target.classList.contains(`js-modal-close`) 28 | ) { 29 | evt.stopPropagation(); 30 | evt.preventDefault(); 31 | 32 | this._handler.click(); 33 | } 34 | }; 35 | 36 | setCloseModalClickHandler(handler) { 37 | this._handler.click = handler; 38 | this.getElement().addEventListener(`click`, this._closeModalClickHandler); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/scripts/views/no-clients-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createNoClientsTemplate = () => { 4 | return ` 5 |
    6 |
    7 | 58 |
    59 |
    60 | There are no clients in our database 61 |
    62 |
    63 | `; 64 | }; 65 | 66 | export default class NoClients extends AbstractView { 67 | getTemplate() { 68 | return createNoClientsTemplate(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/scripts/views/no-search-clients-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createNoSearchClientsTemplate = () => { 4 | return ` 5 |
    6 | There are no clients in our database 7 |
    8 | `; 9 | }; 10 | 11 | export default class NoSearchClients extends AbstractView { 12 | getTemplate() { 13 | return createNoSearchClientsTemplate(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/scripts/views/pagination-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | export default class PaginationView extends AbstractView { 4 | constructor(currentPage = 0, pages = 0) { 5 | super(); 6 | this._currentPage = currentPage; 7 | this._pages = pages; 8 | } 9 | 10 | getTemplate() { 11 | return `
    12 |

    Страница ${this._currentPage} из ${this._pages}

    13 | 16 | 19 |
    `; 20 | } 21 | 22 | _clickHandler = (evt) => { 23 | if (evt.target.tagName !== `BUTTON`) { 24 | return; 25 | } 26 | 27 | evt.preventDefault(); 28 | 29 | this._handler.click(evt.target.dataset.page, this._currentPage); 30 | } 31 | 32 | setPaginationChangeHandler(handler) { 33 | this._handler.click = handler; 34 | this.getElement().addEventListener(`click`, this._clickHandler); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/scripts/views/search-profile-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import Utils from "../utils/common.js"; 3 | import { ContactImage, ContactURL, FieldTitle, Contact } from "../const.js"; 4 | 5 | const getContactImage = (contact) => { 6 | switch (contact.type) { 7 | case Contact.email: 8 | return ContactImage.MAIL; 9 | case Contact.fb: 10 | return ContactImage.FB; 11 | case Contact.vk: 12 | return ContactImage.VK; 13 | case Contact.phone: 14 | return ContactImage.PHONE; 15 | case Contact.additionalPhone: 16 | return ContactImage.PHONE; 17 | default: 18 | return ContactImage.USER; 19 | } 20 | }; 21 | 22 | const getContactURL = (contact) => { 23 | switch (contact.type) { 24 | case Contact.email: 25 | return `mailto:${contact.value}`; 26 | case Contact.fb: 27 | return `${ContactURL.FB}${contact.value}`; 28 | case Contact.vk: 29 | return `${ContactURL.VK}${contact.value}`; 30 | case Contact.phone: 31 | return `tel:${contact.value}`; 32 | case Contact.additionalPhone: 33 | return `tel:${contact.value}`; 34 | default: 35 | return `#`; 36 | } 37 | }; 38 | 39 | const createContactItemTemplate = (contact) => { 40 | if (!contact.value) return; 41 | 42 | return `
  • 43 | 46 | ${getContactImage(contact)} 47 | 53 | ${FieldTitle[contact.type]}: ${ 54 | contact.value 55 | } 56 | 57 | 58 |
  • `; 59 | }; 60 | 61 | const createProductItemTemplate = (client) => `
    62 |
    ${client._id.slice(-6)}
    63 |
    64 | 65 | ${Utils.toUpperCaseFirstLetter( 66 | client.surname 67 | )} ${Utils.toUpperCaseFirstLetter( 68 | client.name 69 | )} ${Utils.toUpperCaseFirstLetter(client.lastname)} 70 | 71 |
    72 |
    73 | 80 |
    81 |
    82 | 83 | 85 | 86 | 89 | 90 | 91 | Изменить 92 |
    93 | 94 | 96 | 97 | 100 | 101 | 102 | Удалить 103 | 104 |
    105 |
    `; 106 | 107 | export default class SearchProfile extends AbstractView { 108 | constructor(client) { 109 | super(); 110 | this._client = client; 111 | } 112 | 113 | getTemplate() { 114 | return createProductItemTemplate(this._client); 115 | } 116 | 117 | _clickProfileHandler = (evt) => { 118 | if (!evt.target.classList.contains(`js-client-profile`)) { 119 | return; 120 | } 121 | 122 | evt.preventDefault(); 123 | this._handler.clickProfile(this._client._id); 124 | }; 125 | 126 | setProfileClientClickHandler(handler) { 127 | this._handler.clickProfile = handler; 128 | 129 | this.getElement().addEventListener(`click`, this._clickProfileHandler); 130 | } 131 | 132 | _clickEditHandler = (evt) => { 133 | if (!evt.target.classList.contains(`js-client-edit`)) { 134 | return; 135 | } 136 | 137 | evt.preventDefault(); 138 | this._handler.clickEdit(this._client._id); 139 | }; 140 | 141 | setEditClientClickHandler(handler) { 142 | this._handler.clickEdit = handler; 143 | 144 | this.getElement().addEventListener(`click`, this._clickEditHandler); 145 | } 146 | 147 | _clickDeleteHandler = (evt) => { 148 | if (!evt.target.classList.contains(`js-client-delete`)) { 149 | return; 150 | } 151 | 152 | evt.preventDefault(); 153 | this._handler.clickDelete(this._client._id); 154 | }; 155 | 156 | setDeleteClientClickHandler(handler) { 157 | this._handler.clickDelete = handler; 158 | 159 | this.getElement().addEventListener(`click`, this._clickDeleteHandler); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /client/scripts/views/smart.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | export default class Smart extends AbstractView { 4 | constructor() { 5 | super(); 6 | 7 | this._state = {}; 8 | } 9 | 10 | restoreHandlers() { 11 | throw new Error(`Abstract method not implemented: restoreHandlers`); 12 | } 13 | 14 | updateData(update, justDataUpdating = false) { 15 | if (!update) { 16 | return; 17 | } 18 | 19 | this._state = Object.assign({}, this._data, update); 20 | 21 | if (!justDataUpdating) { 22 | this.updateElement(); 23 | } 24 | } 25 | 26 | updateElement() { 27 | const oldElement = this.getElement(); 28 | const parentElement = oldElement.parentElement; 29 | const scrollTop = oldElement.scrollTop; 30 | 31 | this.removeElement(); 32 | 33 | const newElement = this.getElement(); 34 | 35 | parentElement.replaceChild(newElement, oldElement); 36 | 37 | newElement.scrollTop = scrollTop; 38 | this.restoreHandlers(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/scripts/views/table-body-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | 3 | const createTableBodyTemplate = () => { 4 | return ` 5 |
    6 | `; 7 | }; 8 | 9 | export default class TableBody extends AbstractView { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | getTemplate() { 15 | return createTableBodyTemplate(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/scripts/views/table-head-view.js: -------------------------------------------------------------------------------- 1 | import AbstractView from "./abstract-view.js"; 2 | import { SortType } from "../const.js"; 3 | 4 | const createSortingListTemplate = (currentSortType) => { 5 | return ` 6 |
    7 |
    8 | 29 | 53 | 78 | 103 |
    Контакты
    104 |
    Действия
    105 |
    106 |
    107 | `; 108 | }; 109 | 110 | export default class TableHead extends AbstractView { 111 | constructor(currentSortType) { 112 | super(); 113 | 114 | this._currentItem = null; 115 | this._currentSortType = currentSortType; 116 | } 117 | 118 | _sortTypeChangeHandler = (evt) => { 119 | evt.preventDefault(); 120 | 121 | const target = evt.target.closest(".js-sort"); 122 | 123 | if (!target) return; 124 | 125 | const eventSortType = target.dataset.sortType; 126 | let newSortType = eventSortType; 127 | 128 | if (eventSortType === SortType.ID_UP) { 129 | newSortType = SortType.ID_DOWN; 130 | target.dataset.sortType = "id-down"; 131 | } else if (eventSortType === SortType.ID_DOWN) { 132 | newSortType = SortType.ID_UP; 133 | target.dataset.sortType = "id-up"; 134 | } 135 | 136 | if (eventSortType === SortType.NAME_UP) { 137 | newSortType = SortType.NAME_DOWN; 138 | target.dataset.sortType = "name-down"; 139 | } else if (eventSortType === SortType.NAME_DOWN) { 140 | newSortType = SortType.NAME_UP; 141 | target.dataset.sortType = "name-up"; 142 | } 143 | 144 | if (eventSortType === SortType.DATE_CREATE_UP) { 145 | newSortType = SortType.DATE_CREATE_DOWN; 146 | target.dataset.sortType = "date-create-down"; 147 | } else if (eventSortType === SortType.DATE_CREATE_DOWN) { 148 | newSortType = SortType.DATE_CREATE_UP; 149 | target.dataset.sortType = "date-create-up"; 150 | } 151 | 152 | if (eventSortType === SortType.DATE_UPDATE_UP) { 153 | newSortType = SortType.DATE_UPDATE_DOWN; 154 | target.dataset.sortType = "date-update-down"; 155 | } else if (eventSortType === SortType.DATE_UPDATE_DOWN) { 156 | newSortType = SortType.DATE_UPDATE_UP; 157 | target.dataset.sortType = "date-update-up"; 158 | } 159 | 160 | this._handler.sortTypeChange(newSortType); 161 | }; 162 | 163 | getTemplate() { 164 | return createSortingListTemplate(this._currentSortType); 165 | } 166 | 167 | setSortTypeChangeHandler = (handler) => { 168 | this._handler.sortTypeChange = handler; 169 | 170 | this.getElement().addEventListener(`click`, this._sortTypeChangeHandler); 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /client/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | --------------------------------------------------------------------------------