diff --git a/package-lock.json b/package-lock.json
index 15211bdad..e68ee39bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,9 @@
"name": "loftschool-example",
"version": "1.0.0",
"license": "MIT",
+ "dependencies": {
+ "@vkid/sdk": "^1.1.0"
+ },
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/eslint-parser": "^7.19.1",
@@ -3304,6 +3307,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@vkid/sdk": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@vkid/sdk/-/sdk-1.1.0.tgz",
+ "integrity": "sha512-D8rDQL2EznT1VZE54+q8bxhbuktxoLsNI5UUpO4U9wOQnXNXaNu/fyr0zK3Ip6oZIyRbob+NXntV1rCp90m8mg==",
+ "dependencies": {
+ "crypto-js": "^4.1.1"
+ }
+ },
"node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -4628,6 +4639,11 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+ },
"node_modules/css-loader": {
"version": "6.7.3",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz",
@@ -14780,6 +14796,14 @@
}
}
},
+ "@vkid/sdk": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@vkid/sdk/-/sdk-1.1.0.tgz",
+ "integrity": "sha512-D8rDQL2EznT1VZE54+q8bxhbuktxoLsNI5UUpO4U9wOQnXNXaNu/fyr0zK3Ip6oZIyRbob+NXntV1rCp90m8mg==",
+ "requires": {
+ "crypto-js": "^4.1.1"
+ }
+ },
"@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -15817,6 +15841,11 @@
"which": "^2.0.1"
}
},
+ "crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+ },
"css-loader": {
"version": "6.7.3",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz",
diff --git a/package.json b/package.json
index 88a301fd2..32e2ffab5 100644
--- a/package.json
+++ b/package.json
@@ -29,10 +29,10 @@
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0",
"@babel/runtime": "^7.21.0",
- "@typescript-eslint/eslint-plugin": "^5.49.0",
- "@typescript-eslint/parser": "^5.49.0",
"@types/jest": "^29.4.0",
"@types/node": "^16.18.13",
+ "@typescript-eslint/eslint-plugin": "^5.49.0",
+ "@typescript-eslint/parser": "^5.49.0",
"babel-jest": "^29.4.3",
"babel-loader": "^9.1.2",
"css-loader": "^6.7.3",
@@ -55,5 +55,8 @@
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
+ },
+ "dependencies": {
+ "@vkid/sdk": "^1.1.0"
}
}
diff --git a/projects/cookie/cookie.html b/projects/cookie/cookie.html
new file mode 100644
index 000000000..766c5451d
--- /dev/null
+++ b/projects/cookie/cookie.html
@@ -0,0 +1,55 @@
+
+
+
+
+ Поиск 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..604ebf82e
--- /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((cookie) => cookie.match(/^([^=]+)=(.+)/))
+ .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.toLowerCase()) &&
+ !value.toLowerCase().includes(filterValue.toLowerCase())
+ ) {
+ 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..cfd42e464
--- /dev/null
+++ b/projects/loft-photo-lite-4/mainPage.js
@@ -0,0 +1,15 @@
+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) {
+ },
+
+ handleEvents() {
+};
diff --git a/projects/loft-photo-lite-4/model.js b/projects/loft-photo-lite-4/model.js
new file mode 100644
index 000000000..c383564e8
--- /dev/null
+++ b/projects/loft-photo-lite-4/model.js
@@ -0,0 +1,21 @@
+const PERM_FRIENDS = 2;
+const PERM_PHOTOS = 4;
+const APP_ID = 5350105;
+
+export default {
+ getRandomElement(array) {},
+
+ async getNextPhoto() {},
+
+ async init() {},
+
+ login() {},
+
+ logout() {},
+
+ getFriends() {},
+
+ getUsers(ids) {},
+
+ async getFriendPhotos(id) {},
+};
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..8614b9a17
--- /dev/null
+++ b/projects/loft-photo-lite-4/profilePage.js
@@ -0,0 +1,9 @@
+import model from './model';
+import mainPage from './mainPage';
+import pages from './pages';
+
+export default {
+ async setUser(user) {},
+
+ handleEvents() {},
+};
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