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}}"> +
+ + {{else}} + <{{this.tag}} class="{{this.className}}" id = "{{this.key}}" data-section="{{this.key}}">{{this.text}} + {{/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}}"> +
+ + {{else}} + <{{this.tag}} class="{{this.className}}" id = "{{this.key}}" data-section="{{this.key}}">{{this.text}} + {{/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)) - + "\">\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)) - + "\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"; -},"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)) - + "\">\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)) - + "\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 @@ Выходной - - + +
    - - - - + + + + + + diff --git a/public/index.js b/public/index.js index 3fff1c2..dae56ec 100644 --- a/public/index.js +++ b/public/index.js @@ -10,10 +10,17 @@ import { RegisterForm } from "./components/Register/Register.js"; import { Header } from "./components/Header/Header.js"; import { Nav } from "./components/Nav/Nav.js"; import { Feed } from "./components/Feed/Feed.js"; +import { Profile } from "./components/Profile/Profile.js"; +import { Search } from "./components/Search/Search.js"; +import { EventContentPage } from "./components/EventContentPage/EventContentPage.js"; +import { UserEventsPage } from "./components/UserEventsPage/UserEventsPage.js"; import { Footer } from "./components/Footer/Footer.js"; import { checkSession } from './modules/session.js'; import { handleRegisterSubmit, handleRegisterCheck } from './modules/registerForm.js'; import { handleLoginSubmit, handleLoginCheck } from './modules/loginForm.js'; +import { EventCreateForm } from "./components/EventCreateForm/EventCreateForm.js"; +import { handleCreateEventSubmit, loadCategories, handleCreateEventEdit } from './modules/handleEventsActions.js'; +import { EditEventForm } from "./components/EditEventForm/EditEventForm.js"; /** * Get the root element @@ -71,7 +78,9 @@ const navigate = (path) => { */ window.dispatchEvent(new PopStateEvent('popstate')); }; - +let header = new Header().renderHeader(userIsLoggedIn, logout, navigate); +root.appendChild(header); +initializeApp(); /** * Update the links container */ @@ -90,29 +99,22 @@ function updateLinksContainer() { header = newHeaderElement; } -/** - * Create the initial header element - */ -let header = new Header().renderHeader(userIsLoggedIn, logout, navigate); -root.appendChild(header); +const newsFeed = document.createElement('main'); -/** - * Create the navigation element - */ -const nav = new Nav().renderNav(); -root.appendChild(nav); +async function initializeApp() { + // Добавление header + // let header = new Header().renderHeader(userIsLoggedIn, logout, navigate); + // root.appendChild(header); -/** - * Create the feed element - */ -const newsFeed = document.createElement('main'); -root.appendChild(newsFeed); + // Добавление навигации + const nav = await new Nav().renderNav(); + root.appendChild(nav); -/** - * Create the footer element - */ -const footer = new Footer().renderFooter(); -root.appendChild(footer); + root.appendChild(newsFeed); + + const footer = new Footer().renderFooter(); + root.appendChild(footer); +} /** * Create the response element @@ -158,9 +160,64 @@ const routes = { */ '/events': async() => { newsFeed.innerHTML = ''; // Clear the modal window content - let feed = await new Feed().renderFeed(); + let feed = await new Feed().renderFeed('/events'); + newsFeed.appendChild(feed); + }, + '/profile': () => { + newsFeed.innerHTML = ''; // Clear the modal window content + const profile = new Profile(); + const profileElement = profile.renderProfile(); + newsFeed.appendChild(profileElement); + }, + '/events/:id': async(id) => { + newsFeed.innerHTML = ''; // Clear the modal window content + + let eventPage = await new EventContentPage('event').renderTemplate(id); + newsFeed.appendChild(eventPage); + }, + '/events/my': async(id) => { + newsFeed.innerHTML = ''; // Clear the modal window content + let UserEventPage = await new UserEventsPage('userEvents').renderTemplate(id); + newsFeed.appendChild(UserEventPage); + let eventPage = await new Feed().renderFeed('/events/my'); + newsFeed.appendChild(eventPage); + }, + '/events/categories/:id': async(id) => { + newsFeed.innerHTML = ''; // Clear the modal window content + let eventPage = await new Feed().renderFeed(`/events/categories/${id}`); + newsFeed.appendChild(eventPage); + }, + '/events/past': async() => { + newsFeed.innerHTML = ''; // Clear the modal window content + let eventPage = await new Feed().renderFeed('/events/past'); + newsFeed.appendChild(eventPage); + }, + '/search': async() => { + newsFeed.innerHTML = ''; // Clear the modal window content + let feed = await new Search().renderSearch('/search', window.location.search.substring(1)); newsFeed.appendChild(feed); }, + '/add_event': async(id) => { + newsFeed.innerHTML = ''; // Clear the modal window content + const categSelect = await loadCategories(); + const formCreate = new EventCreateForm().renderTemplate(categSelect); + newsFeed.appendChild(formCreate); + const createBtn = document.getElementById('eventSubmitBtn'); + createBtn.addEventListener('click', (event) => handleCreateEventSubmit(event, '/events/my', navigate)); + + }, + '/edit_event': async(id) => { + newsFeed.innerHTML = ''; // Clear the modal window content + const categSelect = await loadCategories(); + const formId = 'editEventForm'; + const editEventForm = new EditEventForm(formId); + + const formCreate = editEventForm.renderTemplate(categSelect); + newsFeed.appendChild(formCreate); + await editEventForm.init(id); + const editBtn = document.getElementById('editSubmitBtn'); + editBtn.addEventListener('click', (event) => handleCreateEventEdit(event, id, navigate)); + }, }; /** @@ -178,10 +235,30 @@ const defaultRoute = () => { /** * URL bar listener */ +//This segment is enacted on URL change window.addEventListener('popstate', () => { const path = window.location.pathname; const route = routes[path]; - if (route) { + if (/\/events\/\d+/.test(path)) { + /** + * Call the events route function + */ + const id = path.split('/')[2]; + if (path.split('/')[3] === "edit") { + routes['/edit_event'](id); + } else { + routes['/events/:id'](id); + } + //routes['/events/:id'](id); + } + else if (/\/events\/categories\/\d+/.test(path)) { + /** + * Call the events route function + */ + const id = path.split('/')[3]; + routes['/events/categories/:id'](id); + } + else if (route) { route(); } else { defaultRoute(); // Call the default route if no matching route is found @@ -192,11 +269,11 @@ window.addEventListener('popstate', () => { * Check the current path when the page is loaded */ const currentPath = window.location.pathname; - /** * Check if the current path is the login or signup page */ -if (currentPath === '/login' || currentPath === '/signup') { +//This segment is enacted on refresh +if (currentPath === '/login' || currentPath === '/signup' || currentPath == '/profile' || currentPath == '/search' || currentPath == '/events/my') { /** * Get the route for the current path */ @@ -207,11 +284,33 @@ if (currentPath === '/login' || currentPath === '/signup') { */ route(); } -} else if (currentPath === '/events' || currentPath === "/") { +} else if (currentPath === '/events' || currentPath === '/') { /** * Call the events route function */ routes['/events'](); +} else if (/\/events\/\d+/.test(currentPath)) { + /** + * Call the events route function + */ + const id = currentPath.split('/')[2]; + if (currentPath.split('/')[3] === "edit") { + routes['/edit_event'](id); + } else { + routes['/events/:id'](id); + } // Вызываем обработчик с id +} else if (/\/events\/categories\/\d+/.test(currentPath)) { + /** + * Call the events route function + */ + const id = currentPath.split('/')[3]; + routes['/events/categories/:id'](id); +} else if (currentPath === '/my_events') { + const num = 0; + /* somehow get current user id and check that user is logged in*/ + routes['/events/my'](num); +} else if (currentPath === '/add_event') { + routes['/add_event'](); } else { /** * Call the default route function diff --git a/public/modules/FrontendAPI.js b/public/modules/FrontendAPI.js new file mode 100644 index 0000000..925c1e1 --- /dev/null +++ b/public/modules/FrontendAPI.js @@ -0,0 +1,49 @@ +import { endpoint } from "../config.js"; + +class FrontendAPI { + /* * + {path, headers = {}, needCredentials = false, id = null} + object like request + */ + get(path, request) { + request['method'] = 'GET'; + return this._commonFetchRequest(path, request); + } + + post(path, request) { + request['method'] = 'POST'; + return this._commonFetchRequest(path, request); + } + put(path, request) { + request['method'] = 'PUT'; + return this._commonFetchRequest(path, request); + } + + delete(path, request) { + request['method'] = 'DELETE'; + return this._commonFetchRequest(path, request); + } + + _removeNullUndefined(obj) { + for (const key in obj) { + if (obj[key] === null || obj[key] === undefined) { + delete obj[key]; + } + } + return obj; + } + async _commonFetchRequest(path, request) { + const url = endpoint + path; + const response = await fetch(url, request); + if (!response.ok) { + const errorMessage = await response.text(); + throw { + error: new Error(`Error ${response.status}: ${errorMessage}`), + status: response.status, + }; + } + return response; + } +} + +export const api = new FrontendAPI(); \ No newline at end of file diff --git a/public/modules/handleEventsActions.js b/public/modules/handleEventsActions.js new file mode 100644 index 0000000..5ea8a97 --- /dev/null +++ b/public/modules/handleEventsActions.js @@ -0,0 +1,232 @@ +/** + * Registration module. + * + * This module handles the registration functionality, including form validation and backend requests. + * + * @module registration + */ +/** + * Import form validation functions from the FormValidation.js file + * @import {function} isValidUsername - Checks if a username is valid + * @import {function} isValidPassword - Checks if a password is valid + * @import {function} isValidEmail - Checks if an email is valid + * @import {function} removeDangerous - Removes dangerous characters from a string + */ +import { isValidUsername, isValidPassword, isValidEmail, removeDangerous } from './FormValidation.js'; +/** + * Import the endpoint configuration from the config.js file + * @import {string} endpoint - The API endpoint URL + */ + +import { api } from './FrontendAPI.js'; +/** + * Error message for empty fields. + * @constant {string} + */ +const EMPTY_FIELD = 'Это обязательное поле'; + +/** + * Error message for invalid usernames. + * @constant {string} + */ +const INCORRECT_USERNAME = 'Логин может состоять из латинских букв, цифр и знаков _ и быть в длину не более 15 символов'; + +/** + * Error message for invalid passwords. + * @constant {string} + */ +const INCORRECT_PASSWORD = 'Пароль должен состоят из букв и цифр'; + +/** + * Error message for invalid emails. + * @constant {string} + */ +const INCORRECT_EMAIL = 'Адрес email должен содержать несколько символов до знака @, один символ @, несколько символов после @, точка, несколько знаков послe точки'; + +/** + * Handles the registration form on submission. + * + * This function validates the form data, sends a request to the backend, and handles the response. + * + * @async + * @function handleRegisterSubmit + * @param {Event} event - The form submission event. + * @param {function} setUserLoggedIn - A function to set the user's logged-in state. + * @param {function} navigate - A function to navigate to a different page. + */ + +export 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); + }); + return selectElement; + } catch (error) { + console.error('Ошибка при загрузке категорий:', error); + } + return selectElement; +} + +export async function handleCreateEventEdit(event, id, navigate) { + event.preventDefault(); + loadCategories(); + // Get form data + const title = removeDangerous(document.getElementById('eventNameEntry').value); + const description = removeDangerous(document.getElementById('eventDescriptionEntry').value); + const tag = Array.from(document.getElementById('eventTagEntry').value.split(' '), (tag) => removeDangerous(tag)); + const dateStart = removeDangerous(document.getElementById('eventBeginEntry').value) + ':00Z'; + const dateEnd = removeDangerous(document.getElementById('eventEndEntry').value) + ':00Z'; + const categoryId = Number(removeDangerous(document.getElementById('categoriesInput').value)); + + const image = document.getElementById('imageInput').files[0]; + + try { + // Send request to backend + const userData = { + title: '', + description: description, + tag: tag, + event_start: dateStart, + event_end: dateEnd, + category_id: categoryId, + }; + + 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 = `/events/${id}`; + const response = await api.put(path, request); + // If response is not OK, throw error + if (!response.ok) { + throw new Error(data.message); + } + const data = await response.json(); + if (data.code) { + throw new Error(data.message); + } + // Navigate to page + const pageToCome = `../${id}`; + navigate(pageToCome); + + } catch (error) { + // Display error message if registration fails + document.getElementById('eventServerError').innerText = error; + } + //navigate(pageToCome); //debug +} + +export async function handleCreateEventSubmit(event, pageToCome, navigate) { + event.preventDefault(); + loadCategories(); + // Get form data + const title = removeDangerous(document.getElementById('eventNameEntry').value); + const description = removeDangerous(document.getElementById('eventDescriptionEntry').value); + const tag = Array.from(document.getElementById('eventTagEntry').value.split(' '), (tag) => removeDangerous(tag)); + const dateStart = removeDangerous(document.getElementById('eventBeginEntry').value) + ':00Z'; + const dateEnd = removeDangerous(document.getElementById('eventEndEntry').value) + ':00Z'; + + const categoryId = Number(removeDangerous(document.getElementById('categoriesInput').value)); + + const image = document.getElementById('imageInput').files[0]; + + try { + // Send request to backend + const userData = { + title: title, + description: description, + tag: tag, + event_start: dateStart, + event_end: dateEnd, + category_id: categoryId, + }; + + 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 = '/events'; + const response = await api.post(path, request); + // If response is not OK, throw error + if (!response.ok) { + throw new Error(data.message); + } + const data = await response.json(); + if (data.code) { + throw new Error(data.message); + } + // Navigate to page + navigate(pageToCome); + + } catch (error) { + // Display error message if registration fails + document.getElementById('eventServerError').innerText = error; + } +} + +/** + * Handles the registration form on keyup input validation. + * + * This function dynamically checks the input fields for validity and displays error messages accordingly. + * + * @function handleRegisterCheck + * @param {Event} event - The input event. + */ +export function handleCreateEventCheck(event) { + const target = event.target; + const id = target.id; + + if (id === 'registerUsernameEntry') { + const username = removeDangerous(target.value); + if (!username) { + document.getElementById('registerUsernameError').innerText = EMPTY_FIELD; + } else if (!isValidUsername(username)) { + document.getElementById('registerUsernameError').innerText = INCORRECT_USERNAME; + } else { + document.getElementById('registerUsernameError').innerText = ''; + } + } else if (id === 'registerEmailEntry') { + const email = removeDangerous(target.value); + if (!email) { + document.getElementById('registerEmailError').innerText = EMPTY_FIELD; + } else if (!isValidEmail(email)) { + document.getElementById('registerEmailError').innerText = INCORRECT_EMAIL; + } else { + document.getElementById('registerEmailError').innerText = ''; + } + } else if (id === 'registerPasswordEntry') { + const password = removeDangerous(target.value); + if (!password) { + document.getElementById('registerPasswordError').innerText = EMPTY_FIELD; + } else if (!isValidPassword(password)) { + document.getElementById('registerPasswordError').innerText = INCORRECT_PASSWORD; + } else { + document.getElementById('registerPasswordError').innerText = ''; + } + } +} diff --git a/public/modules/registerForm.js b/public/modules/registerForm.js index 9a68d8e..86abdf4 100644 --- a/public/modules/registerForm.js +++ b/public/modules/registerForm.js @@ -66,6 +66,7 @@ export async function handleRegisterSubmit(event, setUserLoggedIn, navigate) { const username = removeDangerous(document.getElementById('registerUsernameEntry').value); const email = removeDangerous(document.getElementById('registerEmailEntry').value); const password = removeDangerous(document.getElementById('registerPasswordEntry').value); + const image = document.getElementById('imageInput').files[0]; // Initialize validation flag let isValid = true; @@ -97,14 +98,24 @@ export async function handleRegisterSubmit(event, setUserLoggedIn, navigate) { } try { + const userData = { + username: username, + email: email, + password: password, + }; + + const json = JSON.stringify(userData); + const formData = new FormData(); + formData.append('json', json); + formData.append('image', image); // Send request to backend const response = await fetch(`${endpoint}/register`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + }, credentials: 'include', - body: JSON.stringify({ username, email, password }), + body: formData, }); // If response is not OK, throw error if (!response.ok) { diff --git a/public/modules/router.js b/public/modules/router.js new file mode 100644 index 0000000..5b36947 --- /dev/null +++ b/public/modules/router.js @@ -0,0 +1,10 @@ +export const navigate = (path) => { + /** + * Update the URL + */ + window.history.pushState({}, '', path); + /** + * Dispatch a popstate event + */ + window.dispatchEvent(new PopStateEvent('popstate')); +};