diff --git a/.gitignore b/.gitignore index 30bc162..ae9094c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/node_modules \ No newline at end of file +/node_modules +*.precompiled.js 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/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..48dc910 --- /dev/null +++ b/public/components/CategorySelect/CategorySelect.js @@ -0,0 +1,22 @@ +async function loadCategories() { + const selectElement = document.createElement('select'); + try { + const request = { headers: {} }; + const response = await api.get('/categories', request); + const categories = await response.json(); + + console.log(categories); + + // Заполнение выпадающего списка + 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/EventContentPage/EventContentPage.css b/public/components/EventContentPage/EventContentPage.css new file mode 100644 index 0000000..7c8e503 --- /dev/null +++ b/public/components/EventContentPage/EventContentPage.css @@ -0,0 +1,147 @@ +/* Основной контейнер для страницы мероприятия */ +.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; +} + +.event__leftPart__title { + font-size: 36px; + font-weight: bold; + color: #333; + margin-bottom: 10px; + text-align: center; + +} + +.image { + max-width: 80%; + height: auto; + border-radius: 8px; + margin-top: 10px; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); +} + +/* Правая часть с датами, тегами и кнопками */ +.event__rightPart { + width: 100%; + display: inline-block; + align-items: center; + padding-left: 15px; + padding-top: 15px; +} + +.event__date { + padding: 5px; + font-size: 18px; + color: #666; + margin: 8px 0; +} + +.event__tags { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.event__tag { + background-color: #009951; + color: white; + font-size: 20px; + padding: 7px; + border-radius: 15px; +} + + +/* Блок для кнопок */ +.event_actionsDiv { + margin-top: 15px; + display:inline-block; + margin-top: 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..d504948 --- /dev/null +++ b/public/components/EventContentPage/EventContentPage.js @@ -0,0 +1,159 @@ +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; + } + 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: '', + }, + + }; + + + _renderEvent(event) { + //console.log(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; + //console.log(eventImage.src); + 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; + 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 () => { + response = await api.delete(event, event); + console.log(response); + //navigate("/profile", event); + }); + + const editButton = document.createElement('button'); + editButton.className = 'buttonEdit'; + editButton.textContent = 'Редактировать мероприятие'; + editButton.addEventListener("click", () => { + console.log("redact ", event); + const currentPath = window.location.pathname; + navigate(currentPath + "/edit"); + }); + + eventActions.appendChild(editButton); + eventActions.appendChild(deleteButton); + + this.contentBody.appendChild(eventDetails); + this.contentBody.appendChild(eventActions); + + } + async renderTemplate(id) { + + //console.log(id); + const path = `/events/${id}`; + const request = { headers: {} }; + + try { + const response = await api.get(path, request); + + const eventCon = await response.json(); + this._renderEvent(eventCon); + + + console.log(this.btnDeleteEvent); + + this.btnDeleteEvent.addEventListener('click', (event)=>{handleDeleteEventSubmit(event, id, '/events')}); + + } catch (error) { + console.log(error); + console.log("ERROR HERE"); + /* some more useful error handling */ + } + return this.contentBody; + } +} \ No newline at end of file diff --git a/public/components/EventCreateForm/EventCreateForm.css b/public/components/EventCreateForm/EventCreateForm.css new file mode 100644 index 0000000..bc351ff --- /dev/null +++ b/public/components/EventCreateForm/EventCreateForm.css @@ -0,0 +1,5 @@ +.edit_container { + display: flex; + align-items: center; +} + \ 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..cee5ad8 --- /dev/null +++ b/public/components/EventCreateForm/EventCreateForm.js @@ -0,0 +1,266 @@ +/** + * LoginForm class + */ +import { api } from '../../modules/FrontendAPI.js'; +export class EventCreateForm { + + constructor(formId) { + this.form = document.createElement('form'); + this.form.id = formId; + } + + config = { + eventServerError: { + text: '', + tag: 'label', + className: 'error_text', + type: '', + }, + + eventAddLabel: { + + text: 'Создать мероприятие', + + tag: 'label', + + className: '', + + type: '', + }, + + eventNameEntry: { + /** + * Text + * @type {string} + */ + text: 'Название мероприятия', + /** + * Tag type + * @type {string} + */ + tag: 'input', + /** + * Type + * @type {string} + */ + type: 'text', + /** + * Class name + * @type {string} + */ + className: '', + + }, + + eventNameError: { + text: '', + tag: 'label', + className: 'error_text', + type: '', + }, + eventDescriptionEntry: { + + text: 'Описание мероприятия', + tag: 'textarea', + + type: '', + + className: '', + }, + eventDescriptionError: { + /** + * Error text + * @type {string} + */ + text: '', + /** + * Tag type + * @type {string} + */ + tag: 'label', + /** + * Class name + * @type {string} + */ + className: 'error_text', + /** + * Type + * @type {string} + */ + type: '', + }, + + eventTagsEntry: { + + text: 'Тэги (не более 3 штук)', + + tag: 'input', + + type: '', + + className: '', + }, + + eventTagsError: { + + text: '', + + tag: 'label', + + className: 'error_text', + + type: '', + }, + + eventBeginEntry: { + + text: 'Время начала мероприятия', + + tag: 'input', + + type: 'datetime-local', + + className: '', + }, + + eventBeginError: { + + text: '', + + tag: 'label', + + className: 'error_text', + + type: '', + }, + + eventEndEntry: { + /** + * Text + * @type {string} + */ + text: 'Время окончания мероприятия', + /** + * Tag type + * @type {string} + */ + tag: 'input', + /** + * Type + * @type {string} + */ + type: 'datetime-local', + /** + * Class name + * @type {string} + */ + className: '', + }, + + eventEndError: { + /** + * Error text + * @type {string} + */ + text: '', + /** + * Tag type + * @type {string} + */ + tag: 'label', + /** + * Class name + * @type {string} + */ + className: 'error_text', + /** + * Type + * @type {string} + */ + 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" + }, + categories: { + + text: '', + /** + * Tag type + * @type {string} + */ + tag: 'div', + /** + * Class name + * @type {string} + */ + className: '', + /** + * Type + * @type {string} + */ + type: '', + }, + + eventSubmitBtn: { + + text: 'Создать', + + tag: 'button', + + type: 'submit', + + className: '', + }, + }; + + /** + * Renders the form template + * @returns {HTMLFormElement} The rendered form + */ + renderTemplate(selectElement) { + + const template = Handlebars.templates['EventCreateForm.hbs']; + + const config = this.config; + let itemsArray = Object.entries(config); + let items = itemsArray.map(([key, {tag, text, className, type}], index) => { + let needPlaceholder = (tag === 'input'); + let needMaxMinTime = (type === 'time'); + return {key, tag, text, className, type, needPlaceholder, needMaxMinTime}; + }); + console.log(items); + + this.form.innerHTML += template({items}); + + const categoriesSelect = selectElement; + selectElement.id = 'categoriesInput'; + this.form.appendChild(categoriesSelect); + + //const createBtn = document.getElementById('createBtn'); + //createBtn.addEventListener(); + + return this.form; + } + } + \ No newline at end of file diff --git a/public/components/EventCreateForm/EventCreateForm.precompiled.js b/public/components/EventCreateForm/EventCreateForm.precompiled.js new file mode 100644 index 0000000..691667b --- /dev/null +++ b/public/components/EventCreateForm/EventCreateForm.precompiled.js @@ -0,0 +1,70 @@ +(function() { + var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; +templates['EventCreateForm.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 " \n" + + ((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":3,"column":8},"end":{"line":11,"column":15}}})) != null ? stack1 : "") + + "\n"; +},"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 "
\n \n <" + + 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
\n \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":13,"column":9}}})) != null ? stack1 : ""); +},"useData":true}); +})(); \ No newline at end of file diff --git a/public/components/EventEditForm/EventEditForm.css b/public/components/EventEditForm/EventEditForm.css new file mode 100644 index 0000000..e69de29 diff --git a/public/components/EventEditForm/EventEditForm.hbs b/public/components/EventEditForm/EventEditForm.hbs new file mode 100644 index 0000000..e69de29 diff --git a/public/components/EventEditForm/EventEditForm.js b/public/components/EventEditForm/EventEditForm.js new file mode 100644 index 0000000..306a1d0 --- /dev/null +++ b/public/components/EventEditForm/EventEditForm.js @@ -0,0 +1,225 @@ +/** + * LoginForm class + */ +export class LoginForm { + /** + * Creates a new LoginForm instance + * @param {string} formId - The ID of the form + */ + constructor(formId) { + /** + * The form element + * @type {HTMLFormElement} + */ + this.form = document.createElement('form'); + this.form.id = formId; + } + + /** + * Configuration object for the form + * @type {Object} + */ + config = { + /** + * Login server error configuration + * @type {Object} + */ + loginServerError: { + /** + * Error text + * @type {string} + */ + text: '', + /** + * Tag type + * @type {string} + */ + tag: 'label', + /** + * Class name + * @type {string} + */ + className: 'error_text', + /** + * Type + * @type {string} + */ + type: '', + }, + /** + * Login label configuration + * @type {Object} + */ + loginLabel: { + /** + * Text + * @type {string} + */ + text: 'Вход', + /** + * Tag type + * @type {string} + */ + tag: 'label', + /** + * Class name + * @type {string} + */ + className: '', + /** + * Type + * @type {string} + */ + type: '', + }, + /** + * Login username entry configuration + * @type {Object} + */ + loginUsernameEntry: { + /** + * Text + * @type {string} + */ + text: 'Имя пользователя', + /** + * Tag type + * @type {string} + */ + tag: 'input', + /** + * Type + * @type {string} + */ + type: 'text', + /** + * Class name + * @type {string} + */ + className: '', + }, + /** + * Login username error configuration + * @type {Object} + */ + loginUsernameError: { + /** + * Error text + * @type {string} + */ + text: '', + /** + * Tag type + * @type {string} + */ + tag: 'label', + /** + * Class name + * @type {string} + */ + className: 'error_text', + /** + * Type + * @type {string} + */ + type: '', + }, + /** + * Login password entry configuration + * @type {Object} + */ + loginPasswordEntry: { + /** + * Text + * @type {string} + */ + text: 'Пароль', + /** + * Tag type + * @type {string} + */ + tag: 'input', + /** + * Type + * @type {string} + */ + type: 'password', + /** + * Class name + * @type {string} + */ + className: '', + }, + /** + * Login password error configuration + * @type {Object} + */ + loginPasswordError: { + /** + * Error text + * @type {string} + */ + text: '', + /** + * Tag type + * @type {string} + */ + tag: 'label', + /** + * Class name + * @type {string} + */ + className: 'error_text', + /** + * Type + * @type {string} + */ + type: '', + }, + /** + * Login submit button configuration + * @type {Object} + */ + loginSubmitBtn: { + /** + * Text + * @type {string} + */ + text: 'Войти', + /** + * Tag type + * @type {string} + */ + tag: 'button', + /** + * Type + * @type {string} + */ + type: 'submit', + /** + * Class name + * @type {string} + */ + className: '', + }, + }; + + /** + * Renders the form template + * @returns {HTMLFormElement} The rendered form + */ + renderTemplate() { + const template = Handlebars.templates['EventEditForm.hbs']; + const config = this.config; + let itemss = Object.entries(config); + let items = itemss.map(([key, {tag, text, className, type}], index) => { + let needPlaceholder = (tag === 'input'); + return {key, tag, text, className, type, needPlaceholder}; + }); + + this.form.innerHTML += template({items}); + + 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..9485056 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,8 @@ export class Feed { * * @type {Response} */ - const response = await fetch(`${endpoint}/events`, { + console.log(apiPath); + const response = await fetch(`${endpoint}${apiPath}`, { /** * The HTTP method for the request. * @@ -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, description, image} = elem[1]; + const feedElement = new FeedElement(id, description, `${endpoint}/${image}`).renderTemplate(); feedContent.appendChild(feedElement); + feedElement.addEventListener('click', (event) => { + event.preventDefault(); + const path = `/events/${id}`; + navigate(path); + }); }); } else { 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..f9bf976 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,16 @@ 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.src = '/static/images/myavatar.png'; avatarImage.onerror = function() { this.src = "/static/images/default_avatar.png"; this.style.objectFit = 'fill'; @@ -116,6 +133,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 +164,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/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/Profile/Profile.css b/public/components/Profile/Profile.css new file mode 100644 index 0000000..3d7a8e3 --- /dev/null +++ b/public/components/Profile/Profile.css @@ -0,0 +1,85 @@ +/* Общие стили для контейнера профиля */ +#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; +} + +/* Стили для текста ошибок */ +.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..5eefeee --- /dev/null +++ b/public/components/Profile/Profile.js @@ -0,0 +1,159 @@ +import { endpoint } from "../../config.js"; +import { api } from '../../modules/FrontendAPI.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.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: 'Username', id: 'username' }, + { label: 'Email', id: 'email' } + ]; + + 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 = 'Save Changes'; + 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) { + console.log(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]; + console.log("hey"); + 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 username = document.getElementById('username').value; + const email = document.getElementById('email').value; + const image = document.getElementById('profileImage').value; + 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); + + // const response = await fetch(`${endpoint}/profile`, { + // method: 'PUT', + // headers: { + // 'Content-Type': 'application/json' // Заменено на правильный синтаксис + // }, + // body: JSON.stringify({ email: email, username: username }), // Используйте 'body' вместо 'json' + // credentials: 'include', + // }); + + if (response.ok) { + alert('Profile updated successfully!'); + } else { + const errorText = await response.json(); + alert(`Error updating profile: ${errorText.message}`); + } + } catch (error) { + console.error('Error saving profile data:', error); + } + } +} + 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/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..f0395ed --- /dev/null +++ b/public/components/Search/Search.js @@ -0,0 +1,176 @@ +/** + * 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(`/search?tags=${encodeURIComponent(newTags)}&q=${encodeURIComponent(newSearchTerm)}`); + } + }); + // Create the Search title and input field + const searchLabel = document.createElement('label'); + searchLabel.textContent = 'Search'; + searchLabel.className = 'input-label'; // Add a class for styling + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Enter search term...'; // 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(`/search?tags=${encodeURIComponent(newTags)}&q=${encodeURIComponent(newSearchTerm)}`); + } + }); + // 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 + console.log("Tags: ", tags); + console.log("searchTerms: ", searchTerm); + // 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} + */ + console.log(apiPath); + + try { + const request = { + headers: { + + }, + credentials: 'include', + }; + const path = '/events/search?query=' + searchTerm; + const response = await api.get(path, request); + console.log("Search request: ", path); + + 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, description, image} = elem[1]; + const feedElement = new FeedElement(id, description, `${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..8c1910c --- /dev/null +++ b/public/components/UserEventsPage/UserEventsPage.js @@ -0,0 +1,82 @@ +import { api } from "../../modules/FrontendAPI.js"; +import { navigate } from "../../modules/router.js"; +import { FeedElement } from "../FeedElement/FeedElement.js" +import { EventCreateForm } from "../EventCreateForm/EventCreateForm.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) { + console.log(id); + //const path = '/events/'+id.toString(); + //const request = {headers: {}}; + /*try { + const response = await api.get(path, request); + + const event = await response.json(); + console.log(event); + console.log("here"); + + this.renderEvent(event); + //this.leftPart = document.createElement('leftPart'); + //this.rightPart = document.createElement('rightPart'); + + } catch (error) { + console.log(error); + console.log("ERROR HERE"); + + }*/ + const event = {}; + this.renderEvent(event); + console.log(this.contentBody); + return this.contentBody; + } +} \ No newline at end of file diff --git a/public/index.css b/public/index.css index 5a650f0..f3cbb3f 100644 --- a/public/index.css +++ b/public/index.css @@ -2,8 +2,14 @@ @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"; +UserEventsPage body { - font-family: 'Times New Roman', Times, serif; + font-family: Arial, sans-serif; + font-weight: bold; margin: 0; } @@ -30,7 +36,7 @@ body { top: 50%; left: 50%; transform: translate(-50%, -50%); - background: #fff; + background: #f1f1f1; padding: 20px; border: 1px solid #ddd; border-radius: 10px; @@ -76,11 +82,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 { diff --git a/public/index.html b/public/index.html index d985863..71bdbec 100644 --- a/public/index.html +++ b/public/index.html @@ -5,16 +5,17 @@ Выходной - - + +
- - - - + + + + + diff --git a/public/index.js b/public/index.js index 3fff1c2..c3efdb5 100644 --- a/public/index.js +++ b/public/index.js @@ -10,10 +10,16 @@ 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 } from './modules/handleEventsActions.js'; /** * Get the root element @@ -90,29 +96,24 @@ 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); +} + +initializeApp(); /** * Create the response element @@ -158,9 +159,73 @@ 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/:id/edit': async(id) => { + newsFeed.innerHTML = ''; // Clear the modal window content + const categSelect = await loadCategories(); + const formCreate = new EventCreateForm().renderTemplate(categSelect); + console.log(formCreate); + newsFeed.appendChild(formCreate); + const createBtn = document.getElementById('eventSubmitBtn'); + createBtn.addEventListener('click', (event) => handleCreateEventSubmit(event, '/my_events', navigate)); + + }, + '/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('/events', window.location.search.substring(1)); newsFeed.appendChild(feed); }, + '/add_event': async() => { + newsFeed.innerHTML = ''; // Clear the modal window content + const categSelect = await loadCategories(); + const formCreate = new EventCreateForm().renderTemplate(categSelect); + console.log(formCreate); + newsFeed.appendChild(formCreate); + const createBtn = document.getElementById('eventSubmitBtn'); + createBtn.addEventListener('click', (event) => handleCreateEventSubmit(event, '/my_events', navigate)); + + }, + '/edit_event': async() => { + newsFeed.innerHTML = ''; // Clear the modal window content + const categSelect = await loadCategories(); + const formCreate = new EventCreateForm().renderTemplate(categSelect); + console.log(formCreate); + newsFeed.appendChild(formCreate); + const createBtn = document.getElementById('eventSubmitBtn'); + createBtn.addEventListener('click', (event) => handleCreateEventSubmit(event, '/my_events', navigate)); + + }, }; /** @@ -181,7 +246,21 @@ const defaultRoute = () => { 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]; + 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 +271,10 @@ 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') { +if (currentPath === '/login' || currentPath === '/signup' || currentPath == '/profile' || currentPath == '/search') { /** * Get the route for the current path */ @@ -207,11 +285,37 @@ 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]; + routes['/events/:id'](id); // Вызываем обработчик с id +} else if (/\/events\/\d+(\/edit)?/.test(currentPath)) { + /** + * Call the events route function + */ + const id = currentPath.split('/')[2]; + routes['/events/:id/edit'](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') { + //(/\/events\d+/.test(currentPath)) + //let num = currentPath.match(/\d+/)[0]; + 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..6655965 --- /dev/null +++ b/public/modules/FrontendAPI.js @@ -0,0 +1,81 @@ +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; + } + /** + * { method, path, headers = {}, needCredentials = false, body = null, id = null} + */ + async _commonFetchRequest(path, request) { + /* + const id = request.id; + const url = endpoint + request.path + (id ? `/${id}` : ''); + console.log(url); + + const pattern = this._removeNullUndefined(arguments[0]); + console.log(pattern); + const objRequest = {}; + + for (const element in pattern) { + if (element !== null && element !== undefined) { + const name = Object.keys({element})[0]; + objRequest[name] = element; + } + } + + console.log(objRequest); + */ + const url = endpoint + path; + + /* { + method: request.method, + headers: request.headers, + }*/ + + const response = await fetch(url, request); + /*body: JSON.stringify(body), + credentials: needCredentials ? 'include' : '', */ + //const clonedResponse = response.clone(); + + if (!response.ok) { + const errorMessage = await response.text(); + throw { + error: new Error(`Error ${response.status}: ${errorMessage}`), + status: response.status, + }; + } + //const resp = await response.json(); + 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..4700b93 --- /dev/null +++ b/public/modules/handleEventsActions.js @@ -0,0 +1,230 @@ +/** + * 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 { navigate } from "../../modules/router.js"; +/** + * Import the endpoint configuration from the config.js file + * @import {string} endpoint - The API endpoint URL + */ +import { endpoint } from "../../config.js" + +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'); + console.log(selectElement); + try { + const request = { headers: {} }; + + const response = await api.get('/categories', request); + const categories = await response.json(); + + console.log(categories); + + // Заполнение выпадающего списка + 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 handleDeleteEventSubmit(event, id, pageToCome) { + event.preventDefault(); + + try { + // Send request to backend + + const request = { + headers: { + + }, + credentials: 'include', + }; + console.log('ID', id); + const path = '/events'+'/'+id; + const response = await api.delete(path, request); + // If response is not OK, throw error + if (!response.ok) { + throw new Error(data.message); + } + + // Navigate to page + navigate(pageToCome); + + } catch (error) { + // Display error message if registration fails + document.getElementById('eventServerError').innerText = error; + } + //navigate(pageToCome); //debug +} + +export async function handleEditEventSubmit(event, pageToCome, navigate) { + event.preventDefault(); + + // Get form data + const title = removeDangerous(document.getElementById('eventNameEntry').value); + const description = removeDangerous(document.getElementById('eventDescriptionEntry').value); + const tags = removeDangerous(document.getElementById('eventTagsEntry').value).split(); + 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]; + console.log(title, description, tags, dateStart, dateEnd, image, categoryId); + + try { + // Send request to backend + const userData = { + title: title, + description: description, + tags: tags, + 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; + } + //navigate(pageToCome); //debug +} + +export async function handleCreateEventSubmit(event, pageToCome, navigate) { + event.preventDefault(); + + // Get form data + const title = removeDangerous(document.getElementById('eventNameEntry').value); + const description = removeDangerous(document.getElementById('eventDescriptionEntry').value); + const tags = removeDangerous(document.getElementById('eventTagsEntry').value).split(); + 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]; + console.log(title, description, tags, dateStart, dateEnd, image, categoryId); + + try { + // Send request to backend + const userData = { + title: title, + description: description, + tags: tags, + 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; + } +} + diff --git a/public/modules/registerForm.js b/public/modules/registerForm.js index 9a68d8e..2f79a52 100644 --- a/public/modules/registerForm.js +++ b/public/modules/registerForm.js @@ -66,6 +66,8 @@ 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] + console.log(image); // Initialize validation flag let isValid = true; @@ -97,14 +99,25 @@ 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); + console.log(formData); // 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')); +};