├── .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 |
4 |
--------------------------------------------------------------------------------
/client/images/add-contact.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/angle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/button-loader.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/cancel-button-red.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/cancel-button.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/cancel.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/client/images/close-button.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/edit.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/client/images/fb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/images/loader.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/client/images/logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/client/images/mail.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/phone.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/client/images/user-contact.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/images/vk.svg:
--------------------------------------------------------------------------------
1 |
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 |
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: ``,
54 | PHONE: ``,
61 | MAIL: ``,
66 | FB: `
71 | `,
72 | USER: `
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 |
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 |
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 |
93 |
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 ``;
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 |
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 = () => ``;
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 |
8 |
19 |
33 |
44 |
55 |
Контакты
56 |
Действия
57 |
58 |
59 |
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 ``;
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 ``;
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 |
8 |
19 |
33 |
44 |
55 |
Контакты
56 |
Действия
57 |
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 ``;
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 |
72 |
81 |
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 |
--------------------------------------------------------------------------------