diff --git a/.gitignore b/.gitignore
index 30bc162..23c1644 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-/node_modules
\ No newline at end of file
+/node_modules
+/public/components/*/*.precompiled.js
+/public/config.js
\ No newline at end of file
diff --git a/README.md b/README.md
index 9a0277a..e1fdb43 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Frontend проекта "KudaGo" Команда 7
* [Backend](https://github.com/go-park-mail-ru/2024_2_Team7)
# Проект
-* http://37.139.40.252/
+* [vyhodnoy.ru](http://37.139.40.252/)
# figma
* https://www.figma.com/design/B9I0SPwTjYkMcqq6MwO2jW/kudaGo?node-id=0-1&t=DndAvQ4zTz4isemp-1
diff --git a/dockerfile b/dockerfile
index 00f2b5a..0c3025d 100644
--- a/dockerfile
+++ b/dockerfile
@@ -12,9 +12,15 @@ RUN npm install handlebars
# Копируем остальные файлы приложения
COPY . .
+# Запускаем команду Handlebars
+RUN node ./node_modules/handlebars/bin/handlebars public/components/EditEventForm/EditEventForm.hbs -f public/components/EditEventForm/EditEventForm.precompiled.js || echo "Handlebars command failed"
+RUN node ./node_modules/handlebars/bin/handlebars public/components/EventCreateForm/EventCreateForm.hbs -f public/components/EventCreateForm/EventCreateForm.precompiled.js || echo "Handlebars command failed"
+RUN node ./node_modules/handlebars/bin/handlebars public/components/Login/Login.hbs -f public/components/Login/Login.precompiled.js || echo "Handlebars command failed"
+RUN node ./node_modules/handlebars/bin/handlebars public/components/Nav/Nav.hbs -f public/components/Nav/Nav.precompiled.js || echo "Handlebars command failed"
+RUN node ./node_modules/handlebars/bin/handlebars public/components/Register/Register.hbs -f public/components/Register/Register.precompiled.js || echo "Handlebars command failed"
+
# Открываем порт 80
EXPOSE 80
# Команда для запуска приложения
-
CMD ["node", "server/server.js"]
diff --git a/package-lock.json b/package-lock.json
index 8c822d6..f9552b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"cors": "^2.8.5",
"dompurify": "^3.1.7",
- "express": "^4.21.0",
+ "express": "^4.21.1",
"handlebars": "^4.7.8",
"minimist": "^1.2.8",
"node": "^22.9.0"
@@ -542,10 +542,9 @@
}
},
"node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
- "license": "MIT",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
@@ -912,16 +911,16 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -3131,9 +3130,9 @@
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
},
"cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
},
"cookie-signature": {
"version": "1.0.6",
@@ -3377,16 +3376,16 @@
}
},
"express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
diff --git a/package.json b/package.json
index 4955674..2503c6d 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "npx eslint",
- "prepare": "husky && husky install"
+ "prepare": "husky && husky install",
+ "compile:templates": "handlebars public/components/EventCreateForm/EventCreateForm.hbs -f public/components/EventCreateForm/EventCreateForm.precompiled.js"
},
"lint-staged": {
"*.{js, jsx}": [
@@ -32,7 +33,7 @@
"dependencies": {
"cors": "^2.8.5",
"dompurify": "^3.1.7",
- "express": "^4.21.0",
+ "express": "^4.21.1",
"handlebars": "^4.7.8",
"minimist": "^1.2.8",
"node": "^22.9.0"
diff --git a/public/components/CategorySelect/CategorySelect.hbs b/public/components/CategorySelect/CategorySelect.hbs
new file mode 100644
index 0000000..e69de29
diff --git a/public/components/CategorySelect/CategorySelect.js b/public/components/CategorySelect/CategorySelect.js
new file mode 100644
index 0000000..a193213
--- /dev/null
+++ b/public/components/CategorySelect/CategorySelect.js
@@ -0,0 +1,20 @@
+async function loadCategories() {
+ const selectElement = document.createElement('select');
+ try {
+ const request = { headers: {} };
+ const response = await api.get('/categories', request);
+ const categories = await response.json();
+
+ // Заполнение выпадающего списка
+ categories.forEach(category => {
+ const option = document.createElement('option');
+ option.value = category.id; // id категории
+ option.textContent = category.name; // название категории
+ selectElement.appendChild(option);
+ });
+
+ } catch (error) {
+ console.error('Ошибка при загрузке категорий:', error);
+ }
+ return categorySelect;
+}
\ No newline at end of file
diff --git a/public/components/EditEventForm/EditEventForm.css b/public/components/EditEventForm/EditEventForm.css
new file mode 100644
index 0000000..bc351ff
--- /dev/null
+++ b/public/components/EditEventForm/EditEventForm.css
@@ -0,0 +1,5 @@
+.edit_container {
+ display: flex;
+ align-items: center;
+}
+
\ No newline at end of file
diff --git a/public/components/EditEventForm/EditEventForm.hbs b/public/components/EditEventForm/EditEventForm.hbs
new file mode 100644
index 0000000..7827ed5
--- /dev/null
+++ b/public/components/EditEventForm/EditEventForm.hbs
@@ -0,0 +1,13 @@
+{{#each items}}
+
+ {{#if this.needPlaceholder}}
+
+
+ <{{this.tag}} class="{{this.className}}" id = "{{this.key}}" placeholder = "{{this.text}}" type="{{this.type}}" data-section="{{this.key}}">{{this.tag}}>
+
+
+ {{else}}
+ <{{this.tag}} class="{{this.className}}" id = "{{this.key}}" data-section="{{this.key}}">{{this.text}}{{this.tag}}>
+ {{/if}}
+
+{{/each}}
\ No newline at end of file
diff --git a/public/components/EditEventForm/EditEventForm.js b/public/components/EditEventForm/EditEventForm.js
new file mode 100644
index 0000000..cc003ea
--- /dev/null
+++ b/public/components/EditEventForm/EditEventForm.js
@@ -0,0 +1,180 @@
+/**
+ * EditEventForm class
+ */
+import { api } from '../../modules/FrontendAPI.js';
+
+
+export class EditEventForm {
+ constructor(formId) {
+ this.form = document.createElement('form');
+ this.form.id = formId;
+ this.form.className = 'event-edit-form';
+ }
+ async init(id) {
+ const path = `/events/${id}`;
+ const request = { headers: {} };
+ try {
+ const response = await api.get(path, request);
+ const event = await response.json();
+ this._renderEvent(event);
+ } catch (error) {
+ console.log(error);
+ console.log("Ошибка при загрузке события");
+ }
+ }
+ _renderEvent(event) {
+ const mapping = {
+ title: 'eventNameEntry',
+ image: 'imageInput',
+ description: 'eventDescriptionEntry',
+ category_id: 'categories',
+
+ };
+ const time = {
+ event_start: 'eventBeginEntry',
+ event_end: 'eventEndEntry',
+ };
+ for (const key in mapping) {
+ const inputElement = this.form.querySelector(`[id="${mapping[key]}"]`);
+ if (inputElement) {
+ if (mapping[key] === 'imageInput') {
+ inputElement.src = event[key];
+ } else {
+ inputElement.value = event[key];
+ }
+ }
+ }
+ for (const key in time) {
+ const inputElement = this.form.querySelector(`[id="${time[key]}"]`);
+ if (inputElement) {
+ inputElement.value = formatDateTimeForInput(event[key]);
+ }
+ }
+ }
+ config = {
+ eventServerError: {
+ text: '',
+ tag: 'label',
+ className: 'event-edit-form__error-text',
+ type: '',
+ },
+ eventAddLabel: {
+ text: 'Редактировать мероприятие',
+ tag: 'label',
+ className: 'event-edit-form__title',
+ type: '',
+ },
+ eventNameEntry: {
+ text: 'Название мероприятия',
+ tag: 'input',
+ type: 'text',
+ className: 'event-edit-form__input',
+ },
+ eventNameError: {
+ text: '',
+ tag: 'label',
+ className: 'event-edit-form__error-text',
+ type: '',
+ },
+ eventDescriptionEntry: {
+ text: 'Описание мероприятия',
+ tag: 'textarea',
+ type: '',
+ className: 'event-edit-form__textarea',
+ },
+ eventDescriptionError: {
+ text: '',
+ tag: 'label',
+ className: 'event-edit-form__error-text',
+ type: '',
+ },
+ eventTagEntry: {
+ text: 'Тэги (не более 3 штук)',
+ tag: 'input',
+ type: 'text',
+ className: 'event-edit-form__input',
+ },
+ eventTagsError: {
+ text: '',
+ tag: 'label',
+ className: 'event-edit-form__error-text',
+ type: '',
+ },
+ eventBeginEntry: {
+ text: 'Время начала мероприятия',
+ tag: 'input',
+ type: 'datetime-local',
+ className: 'event-edit-form__input event-edit-form__input--datetime-local',
+ },
+ eventBeginError: {
+ text: '',
+ tag: 'label',
+ className: 'event-edit-form__error-text',
+ type: '',
+ },
+ eventEndEntry: {
+ text: 'Время окончания мероприятия',
+ tag: 'input',
+ type: 'datetime-local',
+ className: 'event-edit-form__input event-edit-form__input--datetime-local',
+ },
+ eventEndError: {
+ text: '',
+ tag: 'label',
+ className: 'event-edit-form__error-text',
+ type: '',
+ },
+ imageInput: {
+ text: '',
+ tag: 'input',
+ className: 'event-edit-form__input event-edit-form__input--file',
+ type: 'file',
+ accept: "image/png, image/jpeg",
+ },
+ categories: {
+ text: '',
+ tag: 'div',
+ className: 'event-edit-form__categories-select',
+ type: '',
+ },
+ editSubmitBtn: {
+ text: 'Изменить',
+ tag: 'button',
+ type: 'submit',
+ className: 'event-edit-form__submit-btn',
+ },
+ };
+ /**
+ * Renders the form template
+ * @returns {HTMLFormElement} The rendered form
+ */
+ renderTemplate(selectElement) {
+ const template = Handlebars.templates['EditEventForm.hbs'];
+ const config = this.config;
+ const itemsArray = Object.entries(config);
+ const items = itemsArray.map(([key, { tag, text, className, type }]) => ({
+ key,
+ tag,
+ text,
+ className,
+ type,
+ needPlaceholder: tag === 'input',
+ needMaxMinTime: type === 'time',
+ }));
+ this.form.innerHTML = template({ items });
+ const categoriesSelect = selectElement;
+ categoriesSelect.classList.add('event-edit-form__categories');
+ categoriesSelect.id = 'categoriesInput';
+ this.form.insertBefore(categoriesSelect, this.form.querySelector('.event-edit-form__submit-btn'));
+ return this.form;
+ }
+}
+function formatDateTimeForInput(dateTime) {
+ const date = new Date(dateTime);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
+}
diff --git a/public/components/EventContentPage/EventContentPage.css b/public/components/EventContentPage/EventContentPage.css
new file mode 100644
index 0000000..9cc9285
--- /dev/null
+++ b/public/components/EventContentPage/EventContentPage.css
@@ -0,0 +1,122 @@
+/* Основной контейнер для страницы мероприятия */
+.eventPage {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
+ padding: 20px;
+ margin: 20px auto;
+ max-width: 70%;
+ background-color: #fff;
+ padding: 50px;
+ font-size: large;
+}
+
+.image {
+ max-width: 80%;
+ height: auto;
+ border-radius: 8px;
+ margin-top: 10px;
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.event__date {
+ padding: 5px;
+ font-size: 18px;
+ color: #666;
+ margin: 8px 0;
+}
+
+.event__tags {
+ cursor: pointer;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.event__tag {
+ background-color: #009951;
+ color: white;
+ font-size: 20px;
+ padding: 7px;
+ border-radius: 15px;
+}
+
+/* Кнопка удаления */
+.buttonDelete {
+ background: #fff;
+ border: 2px solid #e74c3c;
+ color: #e74c3c;
+ padding: 10px 20px;
+ border-radius: 20px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.2s ease, color 0.2s ease;
+}
+
+.buttonDelete:hover {
+ background-color: #e74c3c;
+ color: #fff;
+}
+
+/* Кнопка редактирования */
+.buttonEdit {
+ background: #fff;
+ border: 2px solid #ff7b00;
+ color: #ff7b00;
+ padding: 10px 20px;
+ border-radius: 20px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.2s ease, color 0.2s ease;
+}
+
+.buttonEdit:hover {
+ background-color: #ff7b00;
+ color: #fff;
+}
+
+.eventPage {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.event__title {
+ font-size: 36px;
+ font-weight: bold;
+}
+
+.event__details {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.event__image {
+ max-width: 800px;
+ height: auto;
+ margin-top: 10px;
+}
+
+.event__info-row {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ width: 100%;
+ padding: 10px 0;
+}
+
+.event__tags, .event__date {
+ margin: 0 10px;
+}
+
+.event__actions {
+ display: flex;
+ justify-content: center;
+ margin-top: 30px;
+ gap: 350px;
+}
+
diff --git a/public/components/EventContentPage/EventContentPage.js b/public/components/EventContentPage/EventContentPage.js
new file mode 100644
index 0000000..c51c86e
--- /dev/null
+++ b/public/components/EventContentPage/EventContentPage.js
@@ -0,0 +1,178 @@
+import { api } from "../../modules/FrontendAPI.js";
+import { endpoint } from "../../config.js";
+import { navigate } from "../../modules/router.js";
+
+export class EventContentPage {
+ constructor(eventId) {
+ this.contentBody = document.createElement('div');
+ this.contentBody.className = 'eventPage';
+ this.eventId = eventId;
+ }
+ _formatDate(dateString) {
+ const options = { year: 'numeric', month: 'long', day: 'numeric' };
+ return new Date(dateString).toLocaleDateString('ru-RU', options);
+ }
+
+ config = {
+ author: {
+ text: '',
+ tag: 'label',
+ className: '',
+ },
+ dateStart: {
+ text: '',
+ tag: 'label',
+ className: '',
+ },
+ dateEnd: {
+ text: '',
+ tag: 'label',
+ className: '',
+ },
+ title: {
+ text: '',
+ tag: 'label',
+ className: '',
+ },
+ description: {
+ text: '',
+ tag: 'label',
+ className: '',
+ },
+ tag: {
+ text: '',
+ tag: 'label',
+ className: '',
+ },
+ image: {
+ text: '',
+ tag: 'label',
+ className: '',
+ src: '',
+ },
+ };
+
+ async _renderEvent(event) {
+
+ const eventDetails = document.createElement('div');
+ eventDetails.className = 'event__details';
+
+ const eventTitle = document.createElement('div');
+ eventTitle.className = 'event__title';
+ eventTitle.textContent = event.title;
+
+ const eventImage = document.createElement('img');
+ eventImage.className = 'event__image';
+ eventImage.src = endpoint + '/' + event.image;
+ eventImage.onerror = function () {
+ this.src = "/static/images/placeholder.png";
+ this.style.objectFit = 'fill';
+ };
+
+ const tagsDiv = document.createElement('div');
+ tagsDiv.className = 'event__tags';
+ event.tag.forEach(tag => {
+ const tagElement = document.createElement('span');
+ tagElement.className = 'event__tag';
+ tagElement.textContent = tag;
+ tagElement.addEventListener('click', (event) => {
+ event.preventDefault();
+ const path = `/search?tags=${tag}`;
+ navigate(path);
+ })
+ tagsDiv.appendChild(tagElement);
+ });
+
+ const eventStartDate = document.createElement('div');
+ eventStartDate.className = 'event__date';
+ eventStartDate.textContent = `Дата начала: ${this._formatDate(event.event_start)}`;
+
+ const eventEndDate = document.createElement('div');
+ eventEndDate.className = 'event__date';
+ eventEndDate.textContent = `Дата окончания: ${this._formatDate(event.event_end)}`;
+
+
+ const eventInfoRow = document.createElement('div');
+ eventInfoRow.className = 'event__info-row';
+ eventInfoRow.appendChild(tagsDiv);
+ eventInfoRow.appendChild(eventStartDate);
+ eventInfoRow.appendChild(eventEndDate);
+
+ const eventDescription = document.createElement('div');
+ eventDescription.className = 'event__description';
+ eventDescription.textContent = event.description;
+
+ eventDetails.appendChild(eventTitle);
+ eventDetails.appendChild(eventImage);
+ eventDetails.appendChild(eventInfoRow);
+ eventDetails.appendChild(eventDescription);
+
+ const eventActions = document.createElement('div');
+ eventActions.className = 'event__actions';
+
+ const deleteButton = document.createElement('button');
+ deleteButton.className = 'buttonDelete';
+ deleteButton.textContent = 'Удалить мероприятие';
+ deleteButton.addEventListener("click", async () => {
+ const request = {
+ headers: {
+ },
+ credentials: 'include',
+ };
+ const response = await api.delete(`/events/${event.id}`, request);
+ navigate('/events/my');
+ });
+
+ const editButton = document.createElement('button');
+ editButton.className = 'buttonEdit';
+ editButton.textContent = 'Редактировать мероприятие';
+ editButton.addEventListener("click", () => {
+ const currentPath = window.location.pathname;
+ navigate(currentPath + "/edit");
+ });
+
+ const possession = await this.checkPossession();
+ //array of post ids
+ if (possession.includes(event.id)) {
+ eventActions.appendChild(editButton);
+ eventActions.appendChild(deleteButton);
+ }
+
+ this.contentBody.appendChild(eventDetails);
+ this.contentBody.appendChild(eventActions);
+ }
+
+ async renderTemplate(id) {
+ const path = `/events/${id}`;
+ const request = { headers: {} };
+
+ try {
+ const response = await api.get(path, request);
+ const event = await response.json();
+ this._renderEvent(event);
+
+ } catch (error) {
+ console.log(error);
+ console.log("Ошибка при загрузке события");
+ }
+
+ return this.contentBody;
+ }
+ async checkPossession() {
+ const path = `/events/my`;
+ const request = { headers: {}, credentials: "include" };
+
+ try {
+ const response = await api.get(path, request);
+ const event = await response.json();
+ const arr = Array.from(event.events, (ev) => ev.id);
+ return arr;
+
+ } catch (error) {
+ console.log(error);
+ console.log("Ошибка при загрузке событий");
+ }
+
+ return [];
+ }
+}
diff --git a/public/components/EventCreateForm/EventCreateForm.css b/public/components/EventCreateForm/EventCreateForm.css
new file mode 100644
index 0000000..dcc42c2
--- /dev/null
+++ b/public/components/EventCreateForm/EventCreateForm.css
@@ -0,0 +1,81 @@
+.event-create-form {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ max-width: 500px;
+ margin: 50px auto;
+ padding: 20px;
+ background-color: lightgray;
+ border-radius: 10px;
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ .event-create-form__title {
+ font-size: 20px;
+ margin-bottom: 20px;
+ color: black;
+ width: 100%;
+ text-align: center;
+ font-weight: bold;
+ }
+
+ .event-create-form__input {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 20px;
+ font-size: 16px;
+ border: 2px solid #ccc;
+ border-radius: 5px;
+ background-color: #fff;
+ box-sizing: border-box;
+ }
+
+ .event-create-form__textarea {
+ width: 100%;
+ height: 150px;
+ padding: 10px;
+ margin-bottom: 20px;
+ font-size: 16px;
+ border: 2px solid #ccc;
+ border-radius: 5px;
+ background-color: #fff;
+ box-sizing: border-box;
+ resize: vertical;
+ }
+
+ .event-create-form__input:focus,
+ .event-create-form__textarea:focus {
+ border-color: gray;
+ outline: none;
+ }
+
+ .event-create-form__error-text {
+ color: #ff4d4d;
+ font-size: 14px;
+ margin-top: -15px;
+ margin-bottom: 15px;
+ width: 100%;
+ text-align: left;
+ }
+
+ .event-create-form__submit-btn {
+ border-radius: 20px;
+ font-weight: bold;
+ letter-spacing: 1px;
+ border: none;
+ color: white;
+ background-color: black;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ align-self: center;
+ }
+
+ .event-create-form__submit-btn:hover {
+ background-color: gray;
+ }
+
+ .event-create-form__categories {
+ width: 100%;
+ margin-bottom: 20px;
+ font-size: 16px;
+ }
\ No newline at end of file
diff --git a/public/components/EventCreateForm/EventCreateForm.hbs b/public/components/EventCreateForm/EventCreateForm.hbs
new file mode 100644
index 0000000..7827ed5
--- /dev/null
+++ b/public/components/EventCreateForm/EventCreateForm.hbs
@@ -0,0 +1,13 @@
+{{#each items}}
+
+ {{#if this.needPlaceholder}}
+
+
+ <{{this.tag}} class="{{this.className}}" id = "{{this.key}}" placeholder = "{{this.text}}" type="{{this.type}}" data-section="{{this.key}}">{{this.tag}}>
+
+
+ {{else}}
+ <{{this.tag}} class="{{this.className}}" id = "{{this.key}}" data-section="{{this.key}}">{{this.text}}{{this.tag}}>
+ {{/if}}
+
+{{/each}}
\ No newline at end of file
diff --git a/public/components/EventCreateForm/EventCreateForm.js b/public/components/EventCreateForm/EventCreateForm.js
new file mode 100644
index 0000000..5718829
--- /dev/null
+++ b/public/components/EventCreateForm/EventCreateForm.js
@@ -0,0 +1,146 @@
+import { api } from '../../modules/FrontendAPI.js';
+
+export class EventCreateForm {
+ constructor(formId) {
+ this.form = document.createElement('form');
+ this.form.id = formId;
+ this.form.className = 'event-create-form';
+ }
+
+ config = {
+ eventServerError: {
+ text: '',
+ tag: 'label',
+ className: 'event-create-form__error-text',
+ type: '',
+ },
+
+ eventAddLabel: {
+ text: 'Создать мероприятие',
+ tag: 'label',
+ className: 'event-create-form__title',
+ type: '',
+ },
+
+ eventNameEntry: {
+ text: 'Название мероприятия',
+ tag: 'input',
+ type: 'text',
+ className: 'event-create-form__input',
+ },
+
+ eventNameError: {
+ text: '',
+ tag: 'label',
+ className: 'event-create-form__error-text',
+ type: '',
+ },
+
+ eventDescriptionEntry: {
+ text: 'Описание мероприятия',
+ tag: 'textarea',
+ type: '',
+ className: 'event-create-form__textarea',
+ },
+
+ eventDescriptionError: {
+ text: '',
+ tag: 'label',
+ className: 'event-create-form__error-text',
+ type: '',
+ },
+
+ eventTagEntry: {
+ text: 'Тэги (не более 3 штук)',
+ tag: 'input',
+ type: 'text',
+ className: 'event-create-form__input',
+ },
+
+ eventTagsError: {
+ text: '',
+ tag: 'label',
+ className: 'event-create-form__error-text',
+ type: '',
+ },
+
+ eventBeginEntry: {
+ text: 'Время начала мероприятия',
+ tag: 'input',
+ type: 'datetime-local',
+ className: 'event-create-form__input event-create-form__input--datetime-local',
+ },
+
+ eventBeginError: {
+ text: '',
+ tag: 'label',
+ className: 'event-create-form__error-text',
+ type: '',
+ },
+
+ eventEndEntry: {
+ text: 'Время окончания мероприятия',
+ tag: 'input',
+ type: 'datetime-local',
+ className: 'event-create-form__input event-create-form__input--datetime-local',
+ },
+
+ eventEndError: {
+ text: '',
+ tag: 'label',
+ className: 'event-create-form__error-text',
+ type: '',
+ },
+
+ imageInput: {
+ text: '',
+ tag: 'input',
+ className: 'event-create-form__input event-create-form__input--file',
+ type: 'file',
+ accept: 'image/png, image/jpeg',
+ },
+
+ categories: {
+ text: '',
+ tag: 'div',
+ className: 'event-create-form__categories-select',
+ type: '',
+ },
+
+ eventSubmitBtn: {
+ text: 'Создать',
+ tag: 'button',
+ type: 'submit',
+ className: 'event-create-form__submit-btn',
+ },
+ };
+
+ /**
+ * Renders the form template
+ * @returns {HTMLFormElement} The rendered form
+ */
+ renderTemplate(selectElement) {
+ const template = Handlebars.templates['EventCreateForm.hbs'];
+
+ const config = this.config;
+ const itemsArray = Object.entries(config);
+ const items = itemsArray.map(([key, { tag, text, className, type }]) => ({
+ key,
+ tag,
+ text,
+ className,
+ type,
+ needPlaceholder: tag === 'input',
+ needMaxMinTime: type === 'time'
+ }));
+
+ this.form.innerHTML = template({ items });
+
+ const categoriesSelect = selectElement;
+ categoriesSelect.classList.add('event-create-form__categories');
+ categoriesSelect.id = 'categoriesInput';
+ this.form.insertBefore(categoriesSelect, this.form.querySelector('.event-create-form__submit-btn'));
+
+ return this.form;
+ }
+ }
\ No newline at end of file
diff --git a/public/components/Feed/Feed.js b/public/components/Feed/Feed.js
index ee5dc21..a845ced 100644
--- a/public/components/Feed/Feed.js
+++ b/public/components/Feed/Feed.js
@@ -3,6 +3,7 @@
* @import {string} endpoint - The API endpoint URL
*/
import { endpoint } from "../../config.js"
+import { navigate } from "../../modules/router.js";
/**
* Import the FeedElement component from the FeedElement.js file
* @import FeedElement - A component representing a feed element
@@ -33,7 +34,7 @@ export class Feed {
* @method renderFeed
* @returns {HTMLElement} The feed content element.
*/
- async renderFeed() {
+ async renderFeed(apiPath) {
/**
* The feed content element.
*
@@ -53,7 +54,7 @@ export class Feed {
*
* @type {Response}
*/
- const response = await fetch(`${endpoint}/events`, {
+ const response = await fetch(`${endpoint}${apiPath}`, {
/**
* The HTTP method for the request.
*
@@ -68,6 +69,7 @@ export class Feed {
headers: {
//"Content-Type": "application/json",
},
+ credentials: "include",
});
if (response.ok) {
@@ -86,9 +88,15 @@ export class Feed {
* @param {string} description - The description of the event.
* @param {string} image - The image URL of the event.
*/
- Object.entries(feed).forEach(([key, { description, image }]) => {
- const feedElement = new FeedElement(key, description, `${endpoint}${image}`).renderTemplate();
+ Object.entries(feed.events).forEach( (elem) => {
+ const {id, title, image} = elem[1];
+ const feedElement = new FeedElement(id, title, `${endpoint}/${image}`).renderTemplate();
feedContent.appendChild(feedElement);
+ feedElement.addEventListener('click', (event) => {
+ event.preventDefault();
+ const path = `/events/${id}`;
+ navigate(path);
+ });
});
} else {
diff --git a/public/components/FeedElement/FeedElement.js b/public/components/FeedElement/FeedElement.js
index 25e0265..bd4a440 100644
--- a/public/components/FeedElement/FeedElement.js
+++ b/public/components/FeedElement/FeedElement.js
@@ -19,10 +19,10 @@ export class FeedElement {
*
* @constructor
* @param {string} elemId - The ID of the feed element.
- * @param {string} description - The description of the feed element.
+ * @param {string} title - The title of the feed element.
* @param {string} imagePath - The path to the image of the feed element.
*/
- constructor(elemId, description, imagePath) {
+ constructor(elemId, title, imagePath) {
/**
* The feed element.
*
@@ -40,13 +40,13 @@ export class FeedElement {
imageElement.src = imagePath;
/**
- * The description element configuration.
+ * The title element configuration.
*
* @type {Object}
*/
- const descriptionElement = this.config.description;
- descriptionElement.text = description;
- descriptionElement.className = 'description';
+ const titleElement = this.config.title;
+ titleElement.text = title;
+ titleElement.className = 'title';
}
/**
@@ -69,19 +69,19 @@ export class FeedElement {
src: '',
},
/**
- * The description configuration.
+ * The title configuration.
*
* @type {Object}
*/
- description: {
+ title: {
/**
- * The description class name.
+ * The title class name.
*
* @type {string}
*/
className: '',
/**
- * The description text.
+ * The title text.
*
* @type {string}
*/
@@ -92,7 +92,7 @@ export class FeedElement {
/**
* Renders the feed element template.
*
- * This method creates the feed element template and appends the image and description elements to it.
+ * This method creates the feed element template and appends the image and title elements to it.
*
* @method renderTemplate
* @returns {HTMLElement} The feed element.
@@ -114,14 +114,14 @@ export class FeedElement {
this.feedElement.appendChild(imageElement);
/**
- * The description element.
+ * The title element.
*
* @type {HTMLDivElement}
*/
- const descriptionElement = document.createElement('div');
- descriptionElement.className = this.config.description.className;
- descriptionElement.textContent = this.config.description.text;
- this.feedElement.appendChild(descriptionElement);
+ const titleElement = document.createElement('div');
+ titleElement.className = this.config.title.className;
+ titleElement.textContent = this.config.title.text;
+ this.feedElement.appendChild(titleElement);
return this.feedElement;
}
diff --git a/public/components/Header/Header.css b/public/components/Header/Header.css
index 754eacc..8fb3bca 100644
--- a/public/components/Header/Header.css
+++ b/public/components/Header/Header.css
@@ -1,18 +1,10 @@
header {
background-color: white;
- /* Цвет фона */
color: black;
- /* Цвет текста */
display: flex;
- /* Используем flexbox для выравнивания */
justify-content: space-between;
- /* Распределяем пространство между элементами */
align-items: center;
- /* Выравниваем элементы по центру */
- padding: 10px 20px;
- /* Отступы */
- display: grid;
- grid-template-columns: 200px 1fr 1fr;
+ padding: 10px 110px 0 20px;
gap: 20px;
}
@@ -22,21 +14,28 @@ header {
padding-left: 50px;
text-decoration: none;
color: black;
-}
-
-.buttons {
- display: flex;
- justify-content: end;
- align-items: center;
- gap: 20px;
+ margin-left: 30px;
+ flex-basis: 200px;
}
.searchbar {
+ padding: 10px;
height: 40px;
border-radius: 10px;
font-size: 14px;
background-color: lightgray;
border: 1px solid #ccc;
+ flex-grow: 1;
+ margin: 0 20px;
+ min-width: 200px;
+}
+
+.buttons {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 20px;
+ flex-basis: 400px;
}
button {
@@ -44,6 +43,7 @@ button {
padding: 15px 30px;
border-radius: 20px;
font-weight: bold;
+ text-decoration: none;
letter-spacing: 1px;
border: none;
cursor: pointer;
@@ -58,10 +58,11 @@ button {
.btnRegister {
color: white;
background-color: black;
+ margin-right: 85px;
}
.avatar {
width: 50px;
height: 50px;
object-fit: fill;
-}
\ No newline at end of file
+}
diff --git a/public/components/Header/Header.js b/public/components/Header/Header.js
index f64481a..3b4c1ee 100644
--- a/public/components/Header/Header.js
+++ b/public/components/Header/Header.js
@@ -3,6 +3,7 @@
* @import {string} endpoint - The API endpoint URL
*/
import { endpoint } from "../../config.js"
+import { api } from '../../modules/FrontendAPI.js';
/**
* Header module.
*
@@ -59,7 +60,14 @@ export class Header {
searchbar.type = 'search';
searchbar.className = 'searchbar';
searchbar.placeholder = 'Найти событие';
- searchbar.setAttribute('disabled', "");
+ // Add event listener to detect Enter key press
+ searchbar.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') { // Check if the pressed key is Enter
+ event.preventDefault();
+ const searchQuery = searchbar.value;
+ navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
+ }
+ });
headerElement.appendChild(searchbar);
headerElement.appendChild(searchbar);
@@ -98,7 +106,8 @@ export class Header {
btnRegister.className = "btnRegister";
btnRegister.textContent = "Зарегистрироваться";
buttons.appendChild(btnRegister);
- } else {
+ }
+ if (userIsLoggedIn) {
//User is logged in
/**
* The profile link element.
@@ -106,8 +115,15 @@ export class Header {
* @type {HTMLElement}
*/
const profileLink = document.createElement('a');
+ profileLink.href = '/profile';
const avatarImage = document.createElement('img');
- avatarImage.src = '/static/images/myavatar.png';
+
+ this.fetchProfilePic().then(profilePic => {
+ if (profilePic) {
+ avatarImage.src = endpoint + '/' + profilePic.image;
+ }
+ })
+
avatarImage.onerror = function() {
this.src = "/static/images/default_avatar.png";
this.style.objectFit = 'fill';
@@ -116,6 +132,14 @@ export class Header {
avatarImage.className = 'avatar';
profileLink.appendChild(avatarImage);
buttons.appendChild(profileLink);
+
+ const btnMyEvents = document.createElement('button');
+ btnMyEvents.textContent = 'Мои мероприятия';
+ btnMyEvents.addEventListener('click', (event) => {
+ event.preventDefault();
+ const path = '/events/my';
+ navigate(path);
+ });
/**
* The logout button element.
@@ -139,11 +163,31 @@ export class Header {
console.error(error);
}
};
+ buttons.appendChild(btnMyEvents);
buttons.appendChild(logoutButton);
}
headerElement.appendChild(buttons);
return headerElement;
}
+ async fetchProfilePic() {
+ try {
+ const request = {
+ headers: {
+
+ },
+ credentials: 'include',
+ };
+ const response = await api.get('/profile', request);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch profile data');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching profile data:', error);
+ }
+ }
}
\ No newline at end of file
diff --git a/public/components/Login/Login.css b/public/components/Login/Login.css
index c70d6f3..d44dfb7 100644
--- a/public/components/Login/Login.css
+++ b/public/components/Login/Login.css
@@ -14,7 +14,6 @@ form {
form label {
font-size: 18px;
- font-weight: bold;
margin-bottom: 10px;
color: black;
display: block;
@@ -27,12 +26,17 @@ form input {
padding: 10px;
margin-bottom: 20px;
font-size: 16px;
- border: 1px solid #ccc;
+ border: 2px solid #ccc;
border-radius: 5px;
background-color: #fff;
box-sizing: border-box;
}
+form input:focus {
+ border-color: gray;
+ outline: none;
+}
+
form button {
background-color: black;
color: white;
diff --git a/public/components/Login/Login.js b/public/components/Login/Login.js
index 4a74b9f..703554b 100644
--- a/public/components/Login/Login.js
+++ b/public/components/Login/Login.js
@@ -211,8 +211,8 @@ export class LoginForm {
renderTemplate() {
const template = Handlebars.templates['Login.hbs'];
const config = this.config;
- let itemss = Object.entries(config);
- let items = itemss.map(([key, {tag, text, className, type}], index) => {
+ let itemsArray = Object.entries(config);
+ let items = itemsArray.map(([key, {tag, text, className, type}], index) => {
let needPlaceholder = (tag === 'input');
return {key, tag, text, className, type, needPlaceholder};
});
diff --git a/public/components/Login/Login.precompiled.js b/public/components/Login/Login.precompiled.js
deleted file mode 100644
index cf69cd4..0000000
--- a/public/components/Login/Login.precompiled.js
+++ /dev/null
@@ -1,66 +0,0 @@
-(function() {
- var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
-templates['Login.hbs'] = template({"1":function(container,depth0,helpers,partials,data) {
- var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return ((stack1 = lookupProperty(helpers,"if").call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? lookupProperty(depth0,"needPlaceholder") : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data,"loc":{"start":{"line":2,"column":4},"end":{"line":6,"column":11}}})) != null ? stack1 : "");
-},"2":function(container,depth0,helpers,partials,data) {
- var alias1=container.lambda, alias2=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return " <"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + " class=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"className") : depth0), depth0))
- + "\" id = \""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\" placeholder = \""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"text") : depth0), depth0))
- + "\" type=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"type") : depth0), depth0))
- + "\" data-section=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\">"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + ">\n";
-},"4":function(container,depth0,helpers,partials,data) {
- var alias1=container.lambda, alias2=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return " <"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + " class=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"className") : depth0), depth0))
- + "\" id = \""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\" data-section=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\">"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"text") : depth0), depth0))
- + ""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + ">\n";
-},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
- var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return ((stack1 = lookupProperty(helpers,"each").call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? lookupProperty(depth0,"items") : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":7,"column":9}}})) != null ? stack1 : "");
-},"useData":true});
-})();
\ No newline at end of file
diff --git a/public/components/Nav/Nav.css b/public/components/Nav/Nav.css
index 013627c..907153c 100644
--- a/public/components/Nav/Nav.css
+++ b/public/components/Nav/Nav.css
@@ -4,14 +4,22 @@ nav {
height: 70px;
letter-spacing: 1px;
font-size: 18px;
- padding-left: 50px;
- padding-right: 50px;
+ padding: 0 100px 0 90px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
nav ul {
list-style-type: none;
display: flex;
- justify-content: space-around;
+ justify-content: space-between;
+ width: 100%;
+}
+
+nav ul li {
+ display: flex;
+ align-items: center;
}
nav ul li a {
@@ -19,4 +27,5 @@ nav ul li a {
text-decoration: none;
font-weight: bold;
border-radius: 4px;
-}
\ No newline at end of file
+ padding: 10px;
+}
diff --git a/public/components/Nav/Nav.js b/public/components/Nav/Nav.js
index af0853e..55c23e4 100644
--- a/public/components/Nav/Nav.js
+++ b/public/components/Nav/Nav.js
@@ -1,172 +1,57 @@
-/**
- * Nav class
+import { api } from "../../modules/FrontendAPI.js";
+import { navigate } from "../../modules/router.js";
+export class Nav {
+
+ constructor() {
+ this.navElement = document.createElement('nav');
+ this.navigate = navigate;
+ }
+ /**
+ * Renders the navigation template
+ * @returns {HTMLNavElement} The rendered navigation
*/
-export class Nav {
- /**
- * Creates a new Nav instance
- */
- constructor() {
- /**
- * The nav element
- * @type {HTMLNavElement}
- */
- this.navElement = document.createElement('nav');
- /**
- * The ul element
- * @type {HTMLUListElement}
- */
- this.ul = document.createElement('ul');
- }
-
- /**
- * Navigation configuration object
- * @type {Object}
- */
- navigation = {
- /**
- * Popular navigation item configuration
- * @type {Object}
- */
- Popular: {
- /**
- * Link href
- * @type {string}
- */
- href: '/events',
- /**
- * Link text
- * @type {string}
- */
- text: 'Все события',
- },
- /**
- * Exhibition navigation item configuration
- * @type {Object}
- */
- Exhibition: {
- /**
- * Link href
- * @type {string}
- */
- href: '/exhibitions',
- /**
- * Link text
- * @type {string}
- */
- text: 'Выставки',
- },
- /**
- * Theater navigation item configuration
- * @type {Object}
- */
- Theater: {
- /**
- * Link href
- * @type {string}
- */
- href: '/theater',
- /**
- * Link text
- * @type {string}
- */
- text: 'Театр',
- },
- /**
- * Cinema navigation item configuration
- * @type {Object}
- */
- Cinema: {
- /**
- * Link href
- * @type {string}
- */
- href: '/cinema',
- /**
- * Link text
- * @type {string}
- */
- text: 'Кино',
- },
- /**
- * Food navigation item configuration
- * @type {Object}
- */
- Food: {
- /**
- * Link href
- * @type {string}
- */
- href: '/food',
- /**
- * Link text
- * @type {string}
- */
- text: 'Еда',
- },
- /**
- * Kids navigation item configuration
- * @type {Object}
- */
- Kids: {
- /**
- * Link href
- * @type {string}
- */
- href: '/kids',
- /**
- * Link text
- * @type {string}
- */
- text: 'Детям',
- },
- /**
- * Past navigation item configuration
- * @type {Object}
- */
- Past: {
- /**
- * Link href
- * @type {string}
- */
- href: '/past',
- /**
- * Link text
- * @type {string}
- */
- text: 'Прошедшие',
- },
- /**
- * Sport navigation item configuration
- * @type {Object}
- */
- Sport: {
- /**
- * Link href
- * @type {string}
- */
- href: '/sport',
- /**
- * Link text
- * @type {string}
- */
- text: 'Спорт',
- },
+async renderNav() {
+ try {
+
+ const request = { headers: {} };
+ const response = await api.get('/categories', request);
+ const dynamicCategories = await response.json();
+
+ const allEventsItem = {
+ key: 'allEvents',
+ href: '/events',
+ text: 'Все события'
+ };
+
+ const pastEventsItem = {
+ key: 'pastEvents',
+ href: '/events/past',
+ text: 'Прошедшие'
};
-
- /**
- * Renders the navigation template
- * @returns {HTMLNavElement} The rendered navigation
- */
- renderNav() {
- const template = Handlebars.templates['Nav.hbs'];
- const config = this.navigation;
- let itemss = Object.entries(config);
- let items = itemss.map(([key, {href, text}], index) => {
- return {key, href, text};
+
+ const dynamicItems = dynamicCategories.map(category => ({
+ key: category.id || category.name,
+ href: `/events/categories/${category.id}`,
+ text: category.name
+ }));
+
+ const items = [allEventsItem, ...dynamicItems, pastEventsItem];
+
+ const template = Handlebars.templates['Nav.hbs'];
+ this.navElement.innerHTML += template({ items });
+
+ this.navElement.querySelectorAll('a').forEach(link => {
+ link.addEventListener('click', (event) => {
+ event.preventDefault();
+ const path = link.getAttribute('href');
+ this.navigate(path);
});
-
- this.navElement.innerHTML += template({items});
- return this.navElement;
- }
+ });
+
+ return this.navElement;
+ } catch (error) {
+ console.error('Нет категорий:', error);
+ return this.navElement;
}
-
\ No newline at end of file
+}
+}
diff --git a/public/components/Nav/Nav.precompiled.js b/public/components/Nav/Nav.precompiled.js
deleted file mode 100644
index e18f605..0000000
--- a/public/components/Nav/Nav.precompiled.js
+++ /dev/null
@@ -1,30 +0,0 @@
-(function() {
- var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
-templates['Nav.hbs'] = template({"1":function(container,depth0,helpers,partials,data) {
- var alias1=container.lambda, alias2=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return " "
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"text") : depth0), depth0))
- + "\n";
-},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
- var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return "\n"
- + ((stack1 = lookupProperty(helpers,"each").call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? lookupProperty(depth0,"items") : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":2,"column":4},"end":{"line":4,"column":13}}})) != null ? stack1 : "")
- + "
\n";
-},"useData":true});
-})();
\ No newline at end of file
diff --git a/public/components/Profile/Profile.css b/public/components/Profile/Profile.css
new file mode 100644
index 0000000..6fc8368
--- /dev/null
+++ b/public/components/Profile/Profile.css
@@ -0,0 +1,98 @@
+/* Общие стили для контейнера профиля */
+#profileContent {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 800px;
+ min-height: 300px;
+ margin: 50px auto;
+ padding: 20px;
+ background-color: lightgray;
+ border-radius: 10px;
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* Контейнер для фото и формы */
+.profile-container {
+ display: flex;
+ align-items: flex-start;
+}
+
+/* Стили для контейнера фото профиля */
+.profile-picture-container {
+ margin-right: 20px;
+}
+
+/* Стили для фото профиля */
+#profileImage {
+ width: 150px; /* Ширина фото */
+ height: 150px; /* Высота фото */
+ border-radius: 50%; /* Округление */
+ object-fit: cover; /* Обрезка изображения */
+}
+
+/* Стили для формы */
+.form-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%; /* Занимает оставшееся пространство */
+}
+
+/* Стили для меток */
+#profileContent label {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ /* color: black; */
+ display: block;
+ text-align: left;
+ width: 100%;
+}
+
+/* Стили для полей ввода */
+#profileContent input {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 20px;
+ font-size: 16px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ background-color: #fff;
+ box-sizing: border-box;
+}
+
+/* Стили для кнопки сохранения */
+#profileContent button {
+ background-color: black;
+ color: white;
+ padding: 10px 15px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 16px;
+}
+
+/* Эффект при наведении на кнопку */
+#profileContent button:hover {
+ background-color: #444;
+}
+
+/* Стили для успешного сообщения */
+#successMessage {
+ color: green; /* Зеленый цвет для успешного сообщения */
+ font-size: 16px; /* Размер шрифта */
+ margin-top: 10px; /* Отступ сверху */
+}
+/* Стили для сообщения об ошибке */
+#errorMessage {
+ color: red; /* Красный цвет для сообщения об ошибке */
+ font-size: 16px; /* Размер шрифта */
+ margin-top: 10px; /* Отступ сверху */
+}
+
+/* Стили для текста ошибок */
+.error_text {
+ color: #ff4d4d;
+ font-size: 14px;
+ margin-top: 10px;
+}
diff --git a/public/components/Profile/Profile.js b/public/components/Profile/Profile.js
new file mode 100644
index 0000000..3214251
--- /dev/null
+++ b/public/components/Profile/Profile.js
@@ -0,0 +1,165 @@
+import { endpoint } from "../../config.js";
+import { api } from '../../modules/FrontendAPI.js';
+import { navigate } from "../../modules/router.js";
+
+export class Profile {
+ renderProfile() {
+ const profilePage = document.createElement('div');
+ profilePage.id = 'profilePage';
+ const profileContent = document.createElement('div');
+ profileContent.id = 'profileContent';
+
+ const profileContainer = document.createElement('div');
+ profileContainer.classList.add('profile-container');
+
+ const profilePictureContainer = document.createElement('div');
+ profilePictureContainer.classList.add('profile-picture-container');
+
+ const profilePicture = document.createElement('img');
+ profilePicture.id = 'profileImage';
+ profilePicture.onerror = function() {
+ this.src = "/static/images/default_avatar.png";
+ this.style.objectFit = 'fill';
+ };
+ profilePicture.alt = 'Profile Picture';
+
+ // Button to upload new image
+ const uploadButton = document.createElement('input');
+ uploadButton.id = "avatarUpload";
+ uploadButton.type = 'file';
+ uploadButton.accept = 'image/*';
+ uploadButton.addEventListener('change', this.uploadProfilePicture.bind(this));
+
+ profilePictureContainer.appendChild(profilePicture);
+ profilePictureContainer.appendChild(uploadButton);
+
+ const formContainer = document.createElement('div');
+ formContainer.classList.add('form-container');
+
+ // Unchangeable fields
+ const changeableFields = [
+ { label: 'Имя пользователя', id: 'username' },
+ { label: 'Электронная почта', id: 'email' }
+ ];
+
+ const errorMessage = document.createElement('label');
+ errorMessage.id = 'errorMessage';
+ formContainer.appendChild(errorMessage);
+ const successMessage = document.createElement('label');
+ successMessage.id = 'successMessage';
+ formContainer.appendChild(successMessage);
+
+ changeableFields.forEach(field => {
+ const fieldContainer = document.createElement('div');
+ const label = document.createElement('label');
+ label.textContent = field.label;
+ label.setAttribute('for', field.id);
+
+ const input = document.createElement('input');
+ input.type = field.type;
+ input.id = field.id;
+
+ fieldContainer.appendChild(label);
+ fieldContainer.appendChild(input);
+ formContainer.appendChild(fieldContainer);
+ });
+
+ const saveButton = document.createElement('button');
+ saveButton.textContent = 'Сохранить изменения';
+ saveButton.addEventListener('click', this.saveChanges.bind(this));
+ formContainer.appendChild(saveButton);
+
+ profileContainer.appendChild(profilePictureContainer);
+ profileContainer.appendChild(formContainer);
+ profileContent.appendChild(profileContainer);
+
+ this.fetchProfileData().then(profileData => {
+ if (profileData) {
+ document.getElementById('username').value = profileData.username;
+ document.getElementById('email').value = profileData.email;
+ profilePicture.src = endpoint + '/' + profileData.image || '/static/images/default_avatar.png';
+ }
+ }).catch(error => {
+ console.error('Error fetching profile data:', error);
+ });
+
+ profilePage.append(profileContent);
+ return profilePage;
+ }
+
+ async uploadProfilePicture(event) {
+ const file = event.target.files[0];
+ const reader = new FileReader();
+
+ // Set up the onload event for the FileReader
+ reader.onload = (e) => {
+ // Update the src of the profile picture with the uploaded image
+ document.getElementById('profileImage').src = e.target.result;
+ };
+ reader.readAsDataURL(file);
+ }
+
+ async fetchProfileData() {
+ try {
+ const request = {
+ headers: {
+
+ },
+ credentials: 'include',
+ };
+ const response = await api.get('/profile', request);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch profile data');
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching profile data:', error);
+ }
+ }
+
+ async saveChanges() {
+ const profileData = await this.fetchProfileData();
+ let username = '';
+ let email = '';
+ if (document.getElementById('username').value !== profileData.username) {
+ username = document.getElementById('username').value;
+ }
+ if (document.getElementById('email').value !== profileData.email) {
+ email = document.getElementById('email').value;
+ }
+ const image = '' || document.getElementById('avatarUpload').files[0];
+ try {
+ const userData = {
+ email: email,
+ username: username,
+ };
+ const json = JSON.stringify(userData);
+ const formData = new FormData();
+ formData.append('json', json);
+ formData.append('image', image);
+ const body = formData;
+ const request = {
+ headers: {
+
+ },
+ credentials: 'include',
+ body: body,
+ };
+ const path = '/profile';
+ const response = await api.put(path, request);
+
+ if (response.ok) {
+ document.getElementById('successMessage').innerText = 'Профиль успешно обновлён!';
+ document.getElementById('errorMessage').innerText = '';
+ } else {
+ document.getElementById('successMessage').innerText = '';
+ document.getElementById('errorMessage').innerText = 'Ошибка!';
+ const errorText = await response.json();
+ }
+ } catch (error) {
+ document.getElementById('errorMessage').innerText = 'Ошибка сохранения ' + JSON.stringify(error.status);
+ }
+ }
+}
+
diff --git a/public/components/Register/Register.js b/public/components/Register/Register.js
index 0db280f..95afff5 100644
--- a/public/components/Register/Register.js
+++ b/public/components/Register/Register.js
@@ -194,6 +194,29 @@ export class RegisterForm {
*/
type: '',
},
+ imageInput: {
+ /**
+ * Error text
+ * @type {string}
+ */
+ text: '',
+ /**
+ * Tag type
+ * @type {string}
+ */
+ tag: 'input',
+ /**
+ * Class name
+ * @type {string}
+ */
+ className: '',
+ /**
+ * Type
+ * @type {string}
+ */
+ type: 'file',
+ accept: "image/png, image/jpeg"
+ },
/**
* Submit button configuration
diff --git a/public/components/Register/Register.precompiled.js b/public/components/Register/Register.precompiled.js
deleted file mode 100644
index d6403ad..0000000
--- a/public/components/Register/Register.precompiled.js
+++ /dev/null
@@ -1,66 +0,0 @@
-(function() {
- var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
-templates['Register.hbs'] = template({"1":function(container,depth0,helpers,partials,data) {
- var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return ((stack1 = lookupProperty(helpers,"if").call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? lookupProperty(depth0,"needPlaceholder") : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data,"loc":{"start":{"line":2,"column":4},"end":{"line":6,"column":11}}})) != null ? stack1 : "");
-},"2":function(container,depth0,helpers,partials,data) {
- var alias1=container.lambda, alias2=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return " <"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + " class=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"className") : depth0), depth0))
- + "\" id = \""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\" placeholder = \""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"text") : depth0), depth0))
- + "\" type=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"type") : depth0), depth0))
- + "\" data-section=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\">"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + ">\n";
-},"4":function(container,depth0,helpers,partials,data) {
- var alias1=container.lambda, alias2=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return " <"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + " class=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"className") : depth0), depth0))
- + "\" id = \""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\" data-section=\""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"key") : depth0), depth0))
- + "\">"
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"text") : depth0), depth0))
- + ""
- + alias2(alias1((depth0 != null ? lookupProperty(depth0,"tag") : depth0), depth0))
- + ">\n";
-},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
- var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return ((stack1 = lookupProperty(helpers,"each").call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? lookupProperty(depth0,"items") : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":7,"column":9}}})) != null ? stack1 : "");
-},"useData":true});
-})();
\ No newline at end of file
diff --git a/public/components/Search/Search.css b/public/components/Search/Search.css
new file mode 100644
index 0000000..8fa1da1
--- /dev/null
+++ b/public/components/Search/Search.css
@@ -0,0 +1,66 @@
+/* Search.css */
+
+/* Style for the main search page container */
+#searchPage {
+ padding: 20px;
+ background-color: #f9f9f9; /* Light background color */
+}
+
+/* Style for the search parameters container */
+.search-parameters {
+ display: flex;
+ background-color: lightgray;
+ flex-direction: column; /* Stack elements vertically */
+ width: 100%; /* Full width */
+ margin: 20px 0; /* Space above and below the search parameters */
+ padding: 20px; /* Add padding inside the container */
+ border-radius: 10px; /* Curvy corners */
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
+}
+
+/* Style for input labels */
+.input-label {
+ font-weight: bold; /* Make labels bold */
+ margin-bottom: 5px; /* Space between label and input */
+}
+
+/* Style for tags input */
+.tags-input, .search-input {
+ padding: 10px; /* Padding inside the input */
+ border: 1px solid #ccc; /* Border style */
+ border-radius: 5px; /* Rounded corners */
+ margin-bottom: 15px; /* Space between inputs */
+ width: 90%; /* Set width to 90% of the parent container */
+ box-sizing: border-box; /* Include padding and border in width */
+}
+
+/* Style for the feed content area */
+#feedContent {
+ margin-top: 20px; /* Space above the feed content */
+}
+
+/* Optional: Style for individual feed elements */
+.feed-element {
+ padding: 15px; /* Padding for each feed element */
+ border: 1px solid #e0e0e0; /* Light border */
+ border-radius: 4px; /* Rounded corners */
+ margin-bottom: 10px; /* Space between feed elements */
+ background-color: #fff; /* White background */
+}
+
+/* Optional: Hover effect for feed elements */
+.feed-element:hover {
+ background-color: #f1f1f1; /* Light grey on hover */
+}
+
+/* Additional styling for responsiveness */
+@media (max-width: 600px) {
+ .search-parameters {
+ flex-direction: column; /* Stack inputs vertically on small screens */
+ }
+
+ .tags-input,
+ .search-input {
+ margin-bottom: 10px; /* Space between inputs */
+ }
+}
diff --git a/public/components/Search/Search.js b/public/components/Search/Search.js
new file mode 100644
index 0000000..c2fb850
--- /dev/null
+++ b/public/components/Search/Search.js
@@ -0,0 +1,181 @@
+/**
+ * Import the endpoint configuration from the config.js file
+ * @import {string} endpoint - The API endpoint URL
+ */
+import { endpoint } from "../../config.js"
+import { navigate } from "../../modules/router.js";
+import { api } from '../../modules/FrontendAPI.js';
+/**
+ * Import the FeedElement component from the FeedElement.js file
+ * @import FeedElement - A component representing a feed element
+ */
+import { FeedElement } from "../FeedElement/FeedElement.js"
+/**
+ * Feed module.
+ *
+ * This module provides a class to render a feed of events.
+ *
+ * @module feed
+ */
+
+/**
+ * Feed class.
+ *
+ * This class is responsible for rendering a feed of events.
+ *
+ * @class Feed
+ */
+export class Search {
+ /**
+ * Renders the feed of events.
+ *
+ * This method fetches the events from the server, creates a FeedElement for each event, and appends them to the feed content.
+ *
+ * @async
+ * @method renderFeed
+ * @returns {HTMLElement} The feed content element.
+ */
+ async renderSearch(apiPath, searchQuery) {
+ // Create the main container for the search page
+ const searchPage = document.createElement('div');
+ searchPage.id = 'searchPage';
+
+ // Create the search parameters container
+ const searchParameters = document.createElement('div');
+ searchParameters.id = 'searchParameters';
+ searchParameters.className = 'search-parameters'; // Add a class for styling
+
+ // Create the Tags title and input field
+ const tagsLabel = document.createElement('label');
+ tagsLabel.textContent = 'Tags';
+ tagsLabel.className = 'input-label'; // Add a class for styling
+ const tagsInput = document.createElement('input');
+ tagsInput.type = 'text';
+ tagsInput.placeholder = 'Enter tags...'; // Placeholder text for Tags input
+ tagsInput.className = 'tags-input'; // Add a class for styling
+ // Add event listener to detect Enter key press
+ tagsInput.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') { // Check if the pressed key is Enter
+ event.preventDefault();
+ const newTags = tagsInput.value;
+ const newSearchTerm = searchInput.value;
+ navigate(`${apiPath}?q=${encodeURIComponent(newSearchTerm)}&tags=${encodeURIComponent(newTags)}`);
+ }
+ });
+ //Create the Search title and input field
+ const searchLabel = document.createElement('label');
+ searchLabel.textContent = 'Поиск';
+ searchLabel.className = 'input-label'; // Add a class for styling
+ const searchInput = document.createElement('input');
+ searchInput.type = 'text';
+ searchInput.placeholder = 'Введите искомые слова...'; // Placeholder text for Search input
+ searchInput.className = 'search-input'; // Add a class for styling
+ searchInput.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') { // Check if the pressed key is Enter
+ event.preventDefault();
+ const newSearchTerm = searchInput.value;
+ const newTags = tagsInput.value;
+ navigate(`${apiPath}?q=${encodeURIComponent(newSearchTerm)}&tags=${encodeURIComponent(newTags)}`);
+ }
+ });
+ // Parse the searchQuery to extract tags and search term
+ const params = new URLSearchParams(searchQuery);
+
+ const tags = params.get('tags') ? params.get('tags').split(' ') : []; // Split tags by space
+ const searchTerm = params.get('q') || ''; // Get the search term
+ //Set the value of the input fields
+ tagsInput.value = tags.join(' '); // Join tags with space for display
+ searchInput.value = searchTerm; // Set search term
+
+ // Append the titles and input fields to the searchParameters container
+ searchParameters.appendChild(tagsLabel);
+ searchParameters.appendChild(tagsInput);
+ searchParameters.appendChild(searchLabel);
+ searchParameters.appendChild(searchInput);
+
+ // Append the searchParameters to the searchPage
+ searchPage.appendChild(searchParameters);
+ /**
+ * The feed content element.
+ *
+ * @type {HTMLElement}
+ */
+ const feedContent = document.createElement('content');
+
+ /**
+ * Fetches the feed from the server.
+ *
+ * @async
+ * @function fetchFeed
+ */
+ const fetchFeed = async () => {
+ /**
+ * The response from the server.
+ *
+ * @type {Response}
+ */
+
+ try {
+ const request = {
+ headers: {
+
+ },
+ credentials: 'include',
+ };
+ let path = '/events/search?';
+ if (searchTerm) {
+ path += 'query=' + searchTerm;
+ }
+ if (tags.length) {
+ tags.forEach((tag) => {
+ path += '&tags=' + tag;
+ })
+ }
+ //path += '&category_id=' + 7;
+ const response = await api.get(path, request);
+
+ if (response.ok) {
+ /**
+ * The feed data from the server.
+ *
+ * @type {object}
+ */
+ const feed = await response.json();
+ feedContent.id = 'feedContent';
+
+ /**
+ * Iterates over the feed data and creates a FeedElement for each event.
+ *
+ * @param {string} key - The key of the event.
+ * @param {string} description - The description of the event.
+ * @param {string} image - The image URL of the event.
+ */
+ Object.entries(feed.events).forEach( (elem) => {
+ const {id, title, image} = elem[1];
+ const feedElement = new FeedElement(id, title, `${endpoint}/${image}`).renderTemplate();
+ feedContent.appendChild(feedElement);
+ feedElement.addEventListener('click', (event) => {
+ event.preventDefault();
+ const path = `/events/${id}`;
+ navigate(path);
+ });
+ });
+
+ } else {
+ /**
+ * The error text from the server.
+ *
+ * @type {object}
+ */
+ const errorText = await response.json();
+ }
+ } catch (error) {
+ console.error('Error searching:', error);
+ }
+ };
+ await fetchFeed(); // Calls the fetchFeed function
+ searchPage.appendChild(feedContent);
+ return searchPage; // Returns the search page element
+ }
+ }
+
\ No newline at end of file
diff --git a/public/components/UserEventsPage/UserEventsPage.css b/public/components/UserEventsPage/UserEventsPage.css
new file mode 100644
index 0000000..5aa1eb3
--- /dev/null
+++ b/public/components/UserEventsPage/UserEventsPage.css
@@ -0,0 +1,30 @@
+.userEventsManager {
+ margin: calc(var(--group-padding, 0px)* -1);
+ border-radius: inherit;
+ overflow: hidden;
+ isolation: isolate;
+}
+
+.actions {
+ position: relative;
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+ text-align: center;
+}
+
+.createEvent {
+ background: white;
+ border: 2px solid #008EB0;
+ border-color: #008EB0;
+ color:#008EB0;
+
+}
+
+.createEvent:active {
+ background: #008EB0;
+ border: 2px solid #008EB0;
+ border-color: #008EB0;
+ color:white;
+
+}
+
diff --git a/public/components/UserEventsPage/UserEventsPage.js b/public/components/UserEventsPage/UserEventsPage.js
new file mode 100644
index 0000000..e432177
--- /dev/null
+++ b/public/components/UserEventsPage/UserEventsPage.js
@@ -0,0 +1,59 @@
+import { navigate } from "../../modules/router.js";
+
+export class UserEventsPage {
+ constructor(eventId) {
+ this.contentBody = document.createElement('div');
+ this.contentBody.className = 'userEventManager';
+
+ this.eventId = eventId;
+ }
+ config = {
+ createBtn: {
+ },
+ tag: {
+ text: '',
+ tag: 'label',
+ className: 'actions',
+ },
+ image: {
+ text: '',
+ tag: 'label',
+ className: '',
+ src: '',
+ },
+
+ };
+
+ _formUsersEvents(divToAppend) {
+
+ }
+
+ renderEvent(event){
+
+ const createEventDiv = document.createElement('div');
+ createEventDiv.className = 'actions';
+
+ const btnCreate = document.createElement('button');
+ btnCreate.className = 'createEvent';
+ btnCreate.textContent = 'Добавить мероприятие';
+
+ btnCreate.addEventListener('click', (event) => {
+ event.preventDefault();
+ const path = '/add_event';
+ navigate(path);})
+
+ createEventDiv.appendChild(btnCreate);
+
+ const userEventsDiv = document.createElement('div');
+ this._formUsersEvents(userEventsDiv);
+
+ this.contentBody.appendChild(createEventDiv);
+ this.contentBody.appendChild(userEventsDiv);
+
+ }
+ async renderTemplate(id) {
+ const event = {};
+ this.renderEvent(event);
+ return this.contentBody;
+ }
+}
\ No newline at end of file
diff --git a/public/index.css b/public/index.css
index 5a650f0..5a5eef9 100644
--- a/public/index.css
+++ b/public/index.css
@@ -2,8 +2,20 @@
@import "components/Header/Header.css";
@import "components/Nav/Nav.css";
@import "components/Footer/Footer.css";
+@import "components/Profile/Profile.css";
+@import "components/EventContentPage/EventContentPage.css";
+@import "components/UserEventsPage/UserEventsPage.css";
+@import "components/Search/Search.css";
+@import "components/EventCreateForm/EventCreateForm.css";
+@import "components/EventEditForm/EventEditForm.css";
+UserEventsPage
+*{
+ margin:0;
+ padding:0;
+}
body {
- font-family: 'Times New Roman', Times, serif;
+ font-family: Arial, sans-serif;
+ font-weight: bold;
margin: 0;
}
@@ -30,7 +42,7 @@ body {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
- background: #fff;
+ background: #f1f1f1;
padding: 20px;
border: 1px solid #ddd;
border-radius: 10px;
@@ -67,6 +79,7 @@ body {
}
.feed-element {
+ cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
@@ -76,11 +89,7 @@ body {
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
- transition: transform 0.3s ease-in-out;
-}
-
-.feed-element:hover {
- transform: scale(1.05);
+ margin: 20px;
}
.feed-element img {
@@ -104,4 +113,4 @@ body {
color: black;
padding: 20px;
text-align: center;
-}
\ No newline at end of file
+}
diff --git a/public/index.html b/public/index.html
index d985863..d5ad8b6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,16 +5,18 @@
Выходной
-
-
+
+
-
-
-
-
+
+
+
+
+
+