diff --git a/projects/cookie/cookie.html b/projects/cookie/cookie.html new file mode 100644 index 000000000..f93648f2d --- /dev/null +++ b/projects/cookie/cookie.html @@ -0,0 +1,57 @@ + + +
+
+ Поиск cookie: +
+
+
+ Добавить cookie:
+
+
+ +
+
+
+ Доступные cookie: + + + + + + + + + + + +
имязначениеудалить
+
+
+ + diff --git a/projects/cookie/cookie.spec.js b/projects/cookie/cookie.spec.js new file mode 100644 index 000000000..d09d6150f --- /dev/null +++ b/projects/cookie/cookie.spec.js @@ -0,0 +1,250 @@ +const hasOwnProperty = Object.prototype.hasOwnProperty; + +function getCookies() { + return document.cookie + .split('; ') + .filter(Boolean) + .map((cookie) => cookie.match(/^([^=]+)=(.+)/)) + .reduce((obj, [, name, value]) => { + obj[name] = value; + + return obj; + }, {}); +} + +describe('ДЗ 7.2 - Cookie editor', () => { + require('./index'); + + const app = document.querySelector('#app'); + let filterNameInput; + let addNameInput; + let addValueInput; + let addButton; + let listTable; + + describe('Интеграционное тестирование', () => { + beforeEach(() => { + const oldCookies = getCookies(); + + Object.keys(oldCookies).forEach( + (cookie) => (document.cookie = `${cookie}=;expires=${new Date(0)}`) + ); + + if (listTable) { + listTable.innerHTML = ''; + } + }); + + it('на старнице должны быть элементы с нужными id', () => { + filterNameInput = app.querySelector('#filter-name-input'); + addNameInput = app.querySelector('#add-name-input'); + addValueInput = app.querySelector('#add-value-input'); + addButton = app.querySelector('#add-button'); + listTable = app.querySelector('#list-table tbody'); + + expect(filterNameInput).toBeInstanceOf(Element); + expect(addNameInput).toBeInstanceOf(Element); + expect(addValueInput).toBeInstanceOf(Element); + expect(addButton).toBeInstanceOf(Element); + expect(listTable).toBeInstanceOf(Element); + }); + + it('cookie должны добавляться при нажатии на "добавить"', () => { + let cookies; + + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + cookies = getCookies(); + expect(hasOwnProperty.call(cookies, addNameInput.value)); + expect(cookies[addNameInput.value]).toBe(addValueInput.value); + expect(listTable.children.length).toBe(1); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + cookies = getCookies(); + expect(hasOwnProperty.call(cookies, addNameInput.value)); + expect(cookies[addNameInput.value]).toBe(addValueInput.value); + expect(listTable.children.length).toBe(2); + }); + + it('если при добавлении указано имя существующей cookie, то в таблице не должно быть дублей', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + const cookies = getCookies(); + expect(hasOwnProperty.call(cookies, addNameInput.value)); + expect(cookies[addNameInput.value]).toBe(addValueInput.value); + expect(listTable.children.length).toBe(2); + }); + + it('если при добавлении указано имя существующей cookie, то в таблице должно быть изменено ее значение', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'other-test-cookie-value-2'; + addButton.click(); + + const rows = [...listTable.children]; + const changedRow = rows.filter( + (row) => row.children[1].textContent.trim() === 'other-test-cookie-value-2' + ); + expect(changedRow.length).toBe(1); + }); + + it('cookie должны удаляться при нажатии на "удалить"', () => { + let cookies; + let deleteButton; + + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + deleteButton = listTable.querySelector('button'); + + deleteButton.click(); + cookies = getCookies(); + expect(Object.keys(cookies).length).toBe(1); + expect(listTable.children.length).toBe(1); + + deleteButton = listTable.querySelector('button'); + deleteButton.click(); + cookies = getCookies(); + expect(Object.keys(cookies).length).toBe(0); + expect(listTable.children.length).toBe(0); + }); + + describe('Фильтрация', () => { + it('выводить список cookie, имя или значение которых соответствует фильтру', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + filterNameInput.value = 'test-cookie'; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(2); + + filterNameInput.value = 'name-1'; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(1); + + filterNameInput.value = 'name-2'; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(1); + }); + + it('добавлять cookie в таблицу, только если значение cookie соответствует фильтру', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + filterNameInput.value = 'value-2'; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(1); + + addNameInput.value = 'test-cookie-name-3'; + addValueInput.value = 'test-cookie-more-value-2'; + addButton.click(); + + const cookies = getCookies(); + expect(hasOwnProperty.call(cookies, addNameInput.value)); + expect(cookies[addNameInput.value]).toBe(addValueInput.value); + expect(listTable.children.length).toBe(2); + }); + + it('не добавлять cookie в таблицу, если значение cookie не соответствует фильтру', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + filterNameInput.value = 'value-2'; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(2); + + addNameInput.value = 'test-cookie-name-3'; + addValueInput.value = 'test-cookie-value-3'; + addButton.click(); + + const cookies = getCookies(); + expect(hasOwnProperty.call(cookies, addNameInput.value)); + expect(cookies[addNameInput.value]).toBe(addValueInput.value); + expect(listTable.children.length).toBe(1); + }); + + it('удалить cookie из табилицы, если ее значение перестало соответствовать фильтр', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-3'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + filterNameInput.value = 'value-2'; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(2); + + addNameInput.value = 'test-cookie-name-3'; + addValueInput.value = 'test-cookie-value-3'; + addButton.click(); + + const cookies = getCookies(); + expect(hasOwnProperty.call(cookies, addNameInput.value)); + expect(cookies[addNameInput.value]).toBe(addValueInput.value); + expect(listTable.children.length).toBe(1); + }); + + it('выводить все cookie, если фильтр не задан', () => { + addNameInput.value = 'test-cookie-name-1'; + addValueInput.value = 'test-cookie-value-1'; + addButton.click(); + + addNameInput.value = 'test-cookie-name-2'; + addValueInput.value = 'test-cookie-value-2'; + addButton.click(); + + filterNameInput.value = ''; + filterNameInput.dispatchEvent(new KeyboardEvent('input')); + expect(listTable.children.length).toBe(3); + }); + }); + }); +}); diff --git a/projects/cookie/index.js b/projects/cookie/index.js new file mode 100644 index 000000000..d239cc39f --- /dev/null +++ b/projects/cookie/index.js @@ -0,0 +1,135 @@ +/* + ДЗ 7 - Создать редактор cookie с возможностью фильтрации + + 7.1: На странице должна быть таблица со списком имеющихся cookie. Таблица должна иметь следующие столбцы: + - имя + - значение + - удалить (при нажатии на кнопку, выбранная cookie удаляется из браузера и таблицы) + + 7.2: На странице должна быть форма для добавления новой cookie. Форма должна содержать следующие поля: + - имя + - значение + - добавить (при нажатии на кнопку, в браузер и таблицу добавляется новая cookie с указанным именем и значением) + + Если добавляется cookie с именем уже существующей cookie, то ее значение в браузере и таблице должно быть обновлено + + 7.3: На странице должно быть текстовое поле для фильтрации cookie + В таблице должны быть только те cookie, в имени или значении которых, хотя бы частично, есть введенное значение + Если в поле фильтра пусто, то должны выводиться все доступные cookie + Если добавляемая cookie не соответствует фильтру, то она должна быть добавлена только в браузер, но не в таблицу + Если добавляется cookie, с именем уже существующей cookie и ее новое значение не соответствует фильтру, + то ее значение должно быть обновлено в браузере, а из таблицы cookie должна быть удалена + + Запрещено использовать сторонние библиотеки. Разрешено пользоваться только тем, что встроено в браузер + */ + +import './cookie.html'; + +/* + app - это контейнер для всех ваших домашних заданий + Если вы создаете новые html-элементы и добавляете их на страницу, то добавляйте их только в этот контейнер + + Пример: + const newDiv = document.createElement('div'); + homeworkContainer.appendChild(newDiv); + */ +const homeworkContainer = document.querySelector('#app'); +// текстовое поле для фильтрации cookie +const filterNameInput = homeworkContainer.querySelector('#filter-name-input'); +// текстовое поле с именем cookie +const addNameInput = homeworkContainer.querySelector('#add-name-input'); +// текстовое поле со значением cookie +const addValueInput = homeworkContainer.querySelector('#add-value-input'); +// кнопка "добавить cookie" +const addButton = homeworkContainer.querySelector('#add-button'); +// таблица со списком cookie +const listTable = homeworkContainer.querySelector('#list-table tbody'); + +const cookiesMap = getCookies(); +let filterValue = ''; + + +updateTable(); + +function getCookies() { + return document.cookie + .split('; ') + .filter(Boolean) + .map((item) => item.split('=')) + .reduce((obj, [name, value]) => { + obj.set(name, value); + return obj; + }, new Map()); +}; + +filterNameInput.addEventListener('input', () => { + filterValue = filterNameInput.value; + updateTable(); +}); + +addButton.addEventListener('click', () => { + const name = encodeURIComponent(addNameInput.value.trim()); + const value = encodeURIComponent(addValueInput.value.trim()); + + if (!name) { + return; + } + + document.cookie = `${name}=${value}`; + cookiesMap.set(name, value); + + updateTable(); +}); + + +listTable.addEventListener('click', (e) => { + const {role, cookieName} = e.target.dataset; + + if (role === 'remove-cookie') { + cookiesMap.delete(cookieName); + document.cookie = `${cookieName}=deleted; max-age=0`; + updateTable(); + } +}); + +function updateTable() { + const fragment = document.createDocumentFragment(); + let total = 0; + + listTable.innerHTML = ''; + + for (const [name, value] of cookiesMap) { + if (filterValue && + !name.toLowerCase().includes(filterValue.toLocaleLowerCase()) && + !value.toLowerCase().includes(filterValue.toLocaleLowerCase())) { + continue; + } + + total++; + + const tr = document.createElement('tr'); + const nameTD = document.createElement('td'); + const valueTD = document.createElement('td'); + const removeTD = document.createElement('td'); + const removeButton = document.createElement('button'); + + removeButton.dataset.role = 'remove-cookie'; + removeButton.dataset.cookieName = name; + removeButton.textContent = 'Удалить'; + nameTD.textContent = name; + valueTD.textContent = value; + valueTD.classList.add('value'); + tr.append(nameTD, valueTD, removeTD); + removeTD.append(removeButton); + + fragment.append(tr); + } + + if (total) { + listTable.parentNode.classList.remove('hidden'); + listTable.append(fragment); + } else { + listTable.parentNode.classList.add('hidden'); + } +}; + diff --git a/projects/loft-photo-lite-4/images/arrow-left.svg b/projects/loft-photo-lite-4/images/arrow-left.svg new file mode 100644 index 000000000..a4e4c339a --- /dev/null +++ b/projects/loft-photo-lite-4/images/arrow-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/projects/loft-photo-lite-4/images/button.svg b/projects/loft-photo-lite-4/images/button.svg new file mode 100644 index 000000000..6ce85ea9f --- /dev/null +++ b/projects/loft-photo-lite-4/images/button.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/projects/loft-photo-lite-4/images/chat.svg b/projects/loft-photo-lite-4/images/chat.svg new file mode 100644 index 000000000..fc47d01e1 --- /dev/null +++ b/projects/loft-photo-lite-4/images/chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/loft-photo-lite-4/images/exit.svg b/projects/loft-photo-lite-4/images/exit.svg new file mode 100644 index 000000000..d28c122e1 --- /dev/null +++ b/projects/loft-photo-lite-4/images/exit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/projects/loft-photo-lite-4/images/heart-red.svg b/projects/loft-photo-lite-4/images/heart-red.svg new file mode 100644 index 000000000..e9985dca6 --- /dev/null +++ b/projects/loft-photo-lite-4/images/heart-red.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/loft-photo-lite-4/images/heart.svg b/projects/loft-photo-lite-4/images/heart.svg new file mode 100644 index 000000000..4bcdacd80 --- /dev/null +++ b/projects/loft-photo-lite-4/images/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/loft-photo-lite-4/images/logo.svg b/projects/loft-photo-lite-4/images/logo.svg new file mode 100644 index 000000000..12685673d --- /dev/null +++ b/projects/loft-photo-lite-4/images/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/projects/loft-photo-lite-4/images/send.svg b/projects/loft-photo-lite-4/images/send.svg new file mode 100644 index 000000000..5a55b025c --- /dev/null +++ b/projects/loft-photo-lite-4/images/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/loft-photo-lite-4/images/vert1.svg b/projects/loft-photo-lite-4/images/vert1.svg new file mode 100644 index 000000000..d5d86e658 --- /dev/null +++ b/projects/loft-photo-lite-4/images/vert1.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/loft-photo-lite-4/images/vert2.svg b/projects/loft-photo-lite-4/images/vert2.svg new file mode 100644 index 000000000..0f5e75ed2 --- /dev/null +++ b/projects/loft-photo-lite-4/images/vert2.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/loft-photo-lite-4/images/vert3.svg b/projects/loft-photo-lite-4/images/vert3.svg new file mode 100644 index 000000000..7b481af03 --- /dev/null +++ b/projects/loft-photo-lite-4/images/vert3.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/loft-photo-lite-4/index.js b/projects/loft-photo-lite-4/index.js new file mode 100644 index 000000000..b2d110468 --- /dev/null +++ b/projects/loft-photo-lite-4/index.js @@ -0,0 +1,10 @@ +import pages from './pages'; +import mainPage from './mainPage'; +import profilePage from './profilePage'; +import loginPage from './loginPage'; +import('./styles.css'); + +pages.openPage('login'); +loginPage.handleEvents(); +mainPage.handleEvents(); +profilePage.handleEvents(); diff --git a/projects/loft-photo-lite-4/layout.html b/projects/loft-photo-lite-4/layout.html new file mode 100644 index 000000000..89845cfed --- /dev/null +++ b/projects/loft-photo-lite-4/layout.html @@ -0,0 +1,64 @@ + + + + + + Loft Photo + + + +
+ + + +
+ + diff --git a/projects/loft-photo-lite-4/loginPage.js b/projects/loft-photo-lite-4/loginPage.js new file mode 100644 index 000000000..87cf95640 --- /dev/null +++ b/projects/loft-photo-lite-4/loginPage.js @@ -0,0 +1,15 @@ +import model from './model'; +import pages from './pages'; +import mainPage from './mainPage'; + +export default { + handleEvents() { + document.querySelector('.page-login-button').addEventListener('click', async () => { + await model.login(); + await model.init(); + + pages.openPage('main'); + await mainPage.getNextPhoto(); + }); + }, +}; diff --git a/projects/loft-photo-lite-4/mainPage.js b/projects/loft-photo-lite-4/mainPage.js new file mode 100644 index 000000000..fcc598676 --- /dev/null +++ b/projects/loft-photo-lite-4/mainPage.js @@ -0,0 +1,55 @@ +import model from './model'; +import profilePage from './profilePage'; +import pages from './pages'; + +export default { + async getNextPhoto() { + const { friend, id, url } = await model.getNextPhoto(); + this.setFriendAndPhoto(friend, id, url); + }, + + setFriendAndPhoto(friend, id, url) { + const photoComp = document.querySelector('.component-photo'); + const headerPhotoComp = document.querySelector('.component-header-photo'); + const photoNameComp = document.querySelector('.component-header-name'); + const footerPhotoComp = document.querySelector('.component-footer-photo'); + + this.friend = friend; + + photoComp.style.backgrounImage = `url(${url})`; + headerPhotoComp.style.backgrounImage = `url('${frind.photo_50}')`; + photoNameComp.innerText = `${fiend.first_name ?? ''} ${frind.last_name ?? ''}`; + footerPhotoComp.style.backgrounImage = `url('${model.me.photo_50}')`; + }, + + handleEvents() { + let startFrom; + + document.querySelector('.component-photo').addEventListener('touchstart', (e) => { + e.preventDefault(); + startFrom = { y: e.changedTouches[0].pageY }; + }); + + document.querySelector('.component-photo').addEventListener('touchend', async (e) => { + const direction = e.changedTouches[0].pageY - startFrom.y; + if (direction < 0) { + await this.getNextPhoto(); + } + }); + + document + .querySelector('.component-header-profile-link') + .addEventListener('click', async () => { + await profilePage.setUser(this.friend); + pages.openPage('profile'); + }); + + document + .querySelector('.component-footer-container-profile-link') + .addEventListener('click', async () => { + await profilePage.setUser(this.me); + pages.openPage('profile'); + }); + + }, +}; diff --git a/projects/loft-photo-lite-4/model.js b/projects/loft-photo-lite-4/model.js new file mode 100644 index 000000000..80b3933f3 --- /dev/null +++ b/projects/loft-photo-lite-4/model.js @@ -0,0 +1,109 @@ +const PERM_FRIENDS = 2; +const PERM_PHOTOS = 4; +const APP_ID = 5350105; + +export default { + getRandomElement(array) { }, + + async getNextPhoto() { + const friend = this.getRandomElement(this.friends.item); + const photos = await this.getFriendPhotos(friend.id); + const photo = this.getRandomElement(photos.items); + const size = this.findSize(photo); + + return { fiend, id: photo.id, url: size.url }; + }, + + findSize(photo) { + const size = photo.sizes.find((size) => { size.width >= 360 }) + if (!size) { + return photo.sizes.reduce((biggest, current) => { + if (current.width > biggest.width) { + return current; + } + return biggest; + }, photo.sizes[0]); + } + }, + + async init() { + this.photoCache = {}; + this.friends = await this.getFriends(); + [this.me] = await this.getUsers(); + }, + + login() { + return new Promise((resolve, reject) => { + VK.init({ + apiId: APP_ID, + }) + VK.Auth.login((response) => { + if (response.session) { + resolve(response) + } else { + console.error(response); + reject(response); + } + }); + }); + }, + + logout() { + return new Promise ((resolve) => { + return VK.Auth.revokeGrants(resolve); + }); + }, + + callApi(method, params) { + params.v = params.v || '5.120'; + + return new Promise((resolve, reject) => { + VK.api(method, params, (response) => { + if (response.error) { reject(new Error(response.error.error_msg)); } else { resolve(response.response); } + }); + }); + + }, + + getPhotos(owner) { + const params = { + owner_if: owner, + }; + + return this.callApi('photos.getAll', params); + }, + + getFriends() { + const params = { + friends: ['photo_50', 'photo_100'], + } + return this.callApi('friends.get', params); + }, + + async getFriendPhotos(id) { + let photos = this.photoCache[id]; + + if (photos) { + return photos; + } + + photos = await this.getPhotos(id); + + this.photoCache[id] = photos; + + return photos; + }, + + getUsers(ids) { + const params = { + fields: ['photo_50', 'photo_100'] + }; + + if (ids) { + params.user_ids = ids; + } + + return this.callApi('users.get', params); + }, + +}; diff --git a/projects/loft-photo-lite-4/pages.js b/projects/loft-photo-lite-4/pages.js new file mode 100644 index 000000000..7ed7899ab --- /dev/null +++ b/projects/loft-photo-lite-4/pages.js @@ -0,0 +1,9 @@ +const pagesMap = { + login: '.page-login', + main: '.page-main', + profile: '.page-profile', +}; + +export default { + openPage(name) {}, +}; diff --git a/projects/loft-photo-lite-4/profilePage.js b/projects/loft-photo-lite-4/profilePage.js new file mode 100644 index 000000000..2fd74facf --- /dev/null +++ b/projects/loft-photo-lite-4/profilePage.js @@ -0,0 +1,57 @@ +import model from './model'; +import mainPage from './mainPage'; +import pages from './pages'; + +export default { + async setUser(user) { + const photoComp = document.querySelector('.component-user-info-photo'); + const nameComp = document.querySelector('.component-user-info-name'); + const photosComp = document.querySelector('.component-user-photos'); + const photos = await model.getPhotos(user.id); + + this.user = user; + + photoComp.style.background = `url('${user.photo_100}')`; + nameComp.innerText = `${user.first_name ?? ''} ${user.last_name ?? ''}`; + photoComp.innerHTML = ''; + + for(const photo of photos.items) { + const size = model.findSize(photo); + const element = document.createElement('div'); + + element.classList.add('component-user-photo'); + element.dataset.id = photo.id; + element.style.backgroundImage = `url(${size.url})`; + photoComp.append(element); + } + }, + + handleEvents() { + document + .querySelector('.component-user-photos') + .addEventListener('click', async (e) => { + if (e.target.classList.contains('component-user-photo')) { + const photoId = e.target.classList.id; + const friendPhotos = await model.getPhotos(this.user.id); + const photo = friendPhotos.items.find((photo) => photo.id == photoId); + const size = model.findSize(photo); + + mainPage.setFriendAndPhoto(this.iser, parseInt(photoId), size.url); + pages.openPage('main'); + } + }); + + document + .querySelector('.page-profile-back') + .addEventListener('click', async () => { + pages.openPage('main'); + }); + + document + .querySelector('.page-profile-exit') + .addEventListener('click', async () => { + await model.logout(); + pages.openPage('login'); + }) + }, +}; diff --git a/projects/loft-photo-lite-4/readme.md b/projects/loft-photo-lite-4/readme.md new file mode 100644 index 000000000..81b5556a0 --- /dev/null +++ b/projects/loft-photo-lite-4/readme.md @@ -0,0 +1,57 @@ +## Страница профиля + +> Возьмите за основу свое решение из предыдущей недели + +### Часть 1 + +Отобразите собственный аватар на главной странице (правый нижний угол страницы). + +**Шаг 1:** + +Реализуйте метод `getUsers` в файле [model.js](model.js) так, чтобы он возвращал информацию о пользователе по его ID. + +> Для этого вам понадобится метод [users.get](https://dev.vk.com/method/users.get). + +**Шаг 2:** + +В методе `init` модели получите информацию о текущей пользователе (который в данный момент авторизвоан). + +**Шаг 3:** + +На главной странице отобразите аватар текущего пользователя (установите картинку в элемент `.component-footer-photo`) + +### Часть 2 + +Реализуйте страницу профиля. + +На страницу профиля можно перейти если кликнуть на аватар друга (сверху слева) или на собственный аватар (снизу справа). + +Страницы профиля отображает информацию о выбранном пользователе и все его фотографии. + +Так же на странице профиля есть кнопка `назад` (элемент `.page-profile-back`) и кнопка `выйти` (элемент `.page-profile-exit`). + +**Шаг 1:** + +Реализуйте в файле [profilePage.js](profilePage.js) метод `setUser`. + +Этот метод принимает объект с пользователем и отображает информацию о нем: + +- аватар (элемент `.component-user-info-photo`) +- имя и фамилию (элемент `.component-user-info-name`) +- фотографии + + для каждой фотографии необходимо создать элемент `.component-user-photo` и сложить в контейнер `.component-user-photos` + +**Шаг 2:** + +Реализуйте в файле [profilePage.js](profilePage.js) метод `handleEvents`. + +В этом методе добавьте следующие обработчики событий: + +- `клик по фотографии`: открываем главную страницу и отображаем на ней выбранную фотографию и информацию о владельце фотографии +- `клик по кнопке назад`: открываем главную страницу ничего на ней не меняя +- `клик по кнопке выйти`: вызвать метод `logout` модели и открыть стартовую страницу + +**Шаг 3:** + +Реализуйте метод `logout` в файле [model.js](model.js). Метод должен вызывать [VK.Auth.revokeGrants](https://dev.vk.com/api/open-api/getting-started#VK.Auth.revokeGrants) diff --git a/projects/loft-photo-lite-4/styles.css b/projects/loft-photo-lite-4/styles.css new file mode 100644 index 000000000..62d5fba21 --- /dev/null +++ b/projects/loft-photo-lite-4/styles.css @@ -0,0 +1,348 @@ +/* base */ + +body { + font-family: "Roboto Light", Geneva, Arial, Helvetica, sans-serif; +} + +.hidden { + display: none !important; +} + +a { + text-decoration: none; +} + +/* app */ + +#app { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + + align-items: center; + justify-content: center; +} + +.page { + height: 100%; + width: 360px; + position: relative; +} + +/* page login */ + +.page-login { + display: flex; + justify-content: center; + background: #1C1B1F; +} + +.page-login-button { + border: none; + background: url('images/button.svg'); + width: 219px; + height: 40px; + position: absolute; + bottom: 60px; + margin: 0 auto; +} + +.page-login-logo { + top: 429px; + position: absolute; + gap: 16px; + display: flex; + flex-direction: column; + align-items: center; +} + +.page-login-image { + width: 147px; + height: 24px; + background: url('images/logo.svg'); +} + +.page-login-text { + font-size: 14px; + line-height: 20px; + text-align: center; + width: 237px; + color: #B0B0B0; +} + +.page-login-vert1, .page-login-vert2, .page-login-vert3 { + width: 71px; + height: 333px; + position: absolute; +} + +.page-login-vert1 { + top: 59px; + left: 49px; + background: linear-gradient(180deg, rgba(28, 27, 31, 0) 80%, #1C1B1F 100%), url('images/vert1.svg'); +} + +.page-login-vert2 { + top: 81px; + left: 144px; + background: linear-gradient(180deg, rgba(28, 27, 31, 0) 80%, #1C1B1F 100%), url('images/vert2.svg'); +} + +.page-login-vert3 { + top: 59px; + left: 239px; + background: linear-gradient(180deg, rgba(28, 27, 31, 0) 80%, #1C1B1F 100%), url('images/vert3.svg'); +} + +/* page main */ + +.page-main .component-header { + position: absolute; + display: flex; + height: 80px; + top: 0; + left: 0; + right: 0; + background: rgba(0 0 0 / 25%); + padding: 0 24px; +} + +.page-main .component-header-profile-link { + display: flex; + align-items: center; +} + +.page-main .component-header-photo { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.page-main .component-header-name { + margin-left: 8px; + font-weight: 400; + font-size: 16px; + color: white; +} + +.page-main .component-footer { + position: absolute; + display: flex; + height: 80px; + bottom: 0; + left: 0; + right: 0; + background: rgba(0 0 0 / 25%); + padding: 0 24px; +} + +.page-main .component-footer-container { + display: flex; + align-items: center; + width: 100%; +} + +.page-main .component-footer-container-profile-link { + margin-left: auto; +} + +.page-main .component-footer-photo { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.page-main .component-footer-container-social-comments, +.page-main .component-footer-container-social-likes { + color: white; + display: flex; + align-items: center; +} + +.page-main .component-footer-container-social-comments:before, +.page-main .component-footer-container-social-likes:before { + display: inline-block; + content: ''; + width: 20px; + height: 20px; + margin-right: 6px; +} + +.page-main .component-footer-container-social-comments:before { + background: url("images/chat.svg"); +} + +.page-main .component-footer-container-social-likes:before { + background: url("images/heart.svg"); + margin-left: 18px; +} + +.page-main .component-footer-container-social-likes.liked:before { + background: url("images/heart-red.svg"); + margin-left: 18px; +} + +.page-main .component-photo { + height: 100%; + width: 360px; + position: relative; + + background-size: cover; + background-position: center; +} + +.component-comments { + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: 0; + background: rgba(0, 0, 0, 0.4); +} + +.component-comments-container { + position: absolute; + display: flex; + flex-direction: column; + top: 50vh; + bottom: 0; + left: 0; + right: 0; + padding: 16px; + border-radius: 28px 28px 0 0; + background: white; +} + +.component-comments-container-title { + font-size: 14px; + text-align: center; + width: 100%; +} + +.component-comments-container-list { + margin-top: 24px; + flex-grow: 1; + display: flex; + gap: 12px; + flex-direction: column; + overflow-y: auto; + margin-bottom: 14px +} + +.component-comments-container-form { + display: flex; + align-items: center; + gap: 16px; + height: 48px; +} + +.component-comments-container-form-input { + box-sizing: border-box; + border: 1px solid #E0E0E0; + border-radius: 32px; + flex-grow: 1; + height: 48px; +} + +.component-comments-container-form-input, +.component-comments-container-form-input, +.component-comments-container-form-input, +.component-comments-container-form-input { + padding: 14px 16px; +} + +.component-comments-container-form-send { + background: url('images/send.svg'); + width: 40px; + height: 40px; +} + +.component-comment { + display: flex; + gap: 8px +} + +.component-comment-photo { + width: 24px; + height: 24px; + border-radius: 50%; + background-position: center; + background-size: cover; +} + +.component-comment-content { + flex-direction: column; +} + +.component-comment-name { + font-size: 12px; +} + +.component-comment-text { + font-size: 14px; +} + +/* page profile */ + +.page-profile { + margin-top: 52px; +} + +.page-profile-back { + background: url('images/arrow-left.svg'); + width: 24px; + height: 24px; + + position: absolute; + left: 24px; +} + +.page-profile-exit { + background: url('images/exit.svg'); + width: 24px; + height: 24px; + + position: absolute; + right: 24px; +} + +.component-user-photos { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 24px 16px 16px 16px; +} + +.component-user-photo { + width: 104px; + height: 104px; + background-size: cover; + background-repeat: no-repeat; + background-position: center; +} + +.page-profile .component-user-info { + display: flex; + flex-direction: column; + align-items: center; +} + +.page-profile .component-user-info-photo { + height: 72px; + width: 72px; + border-radius: 50%; + + background-size: cover; + background-position: center; +} + +.page-profile .component-user-info-name { + font-weight: 400; + font-size: 18px; + line-height: 26px; + margin-top: 8px; +} \ No newline at end of file