diff --git a/projects/loft-photo-lite-3/images/arrow-left.svg b/projects/loft-photo-lite-3/images/arrow-left.svg
new file mode 100644
index 000000000..a4e4c339a
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/arrow-left.svg
@@ -0,0 +1,4 @@
+
diff --git a/projects/loft-photo-lite-3/images/button.svg b/projects/loft-photo-lite-3/images/button.svg
new file mode 100644
index 000000000..6ce85ea9f
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/button.svg
@@ -0,0 +1,17 @@
+
diff --git a/projects/loft-photo-lite-3/images/chat.svg b/projects/loft-photo-lite-3/images/chat.svg
new file mode 100644
index 000000000..fc47d01e1
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/chat.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo-lite-3/images/exit.svg b/projects/loft-photo-lite-3/images/exit.svg
new file mode 100644
index 000000000..d28c122e1
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/exit.svg
@@ -0,0 +1,5 @@
+
diff --git a/projects/loft-photo-lite-3/images/heart-red.svg b/projects/loft-photo-lite-3/images/heart-red.svg
new file mode 100644
index 000000000..e9985dca6
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/heart-red.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo-lite-3/images/heart.svg b/projects/loft-photo-lite-3/images/heart.svg
new file mode 100644
index 000000000..4bcdacd80
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/heart.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo-lite-3/images/logo.svg b/projects/loft-photo-lite-3/images/logo.svg
new file mode 100644
index 000000000..12685673d
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/logo.svg
@@ -0,0 +1,11 @@
+
diff --git a/projects/loft-photo-lite-3/images/send.svg b/projects/loft-photo-lite-3/images/send.svg
new file mode 100644
index 000000000..5a55b025c
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/send.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo-lite-3/images/vert1.svg b/projects/loft-photo-lite-3/images/vert1.svg
new file mode 100644
index 000000000..d5d86e658
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/vert1.svg
@@ -0,0 +1,22 @@
+
diff --git a/projects/loft-photo-lite-3/images/vert2.svg b/projects/loft-photo-lite-3/images/vert2.svg
new file mode 100644
index 000000000..0f5e75ed2
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/vert2.svg
@@ -0,0 +1,22 @@
+
diff --git a/projects/loft-photo-lite-3/images/vert3.svg b/projects/loft-photo-lite-3/images/vert3.svg
new file mode 100644
index 000000000..7b481af03
--- /dev/null
+++ b/projects/loft-photo-lite-3/images/vert3.svg
@@ -0,0 +1,22 @@
+
diff --git a/projects/loft-photo-lite-3/index.js b/projects/loft-photo-lite-3/index.js
new file mode 100644
index 000000000..ba96b1476
--- /dev/null
+++ b/projects/loft-photo-lite-3/index.js
@@ -0,0 +1,8 @@
+import pages from './pages';
+import mainPage from './mainPage';
+import loginPage from './loginPage';
+import('./styles.css');
+
+pages.openPage('login');
+loginPage.handleEvents();
+mainPage.handleEvents();
diff --git a/projects/loft-photo-lite-3/layout.html b/projects/loft-photo-lite-3/layout.html
new file mode 100644
index 000000000..89845cfed
--- /dev/null
+++ b/projects/loft-photo-lite-3/layout.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Loft Photo
+
+
+
+
+
+
+
+
+
+
+
Не пропустите лучшие моменты из жизни ваших друзей!
+
+
+
+
+
+
+
+
diff --git a/projects/loft-photo-lite-3/loginPage.js b/projects/loft-photo-lite-3/loginPage.js
new file mode 100644
index 000000000..87cf95640
--- /dev/null
+++ b/projects/loft-photo-lite-3/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-3/mainPage.js b/projects/loft-photo-lite-3/mainPage.js
new file mode 100644
index 000000000..ba2710ced
--- /dev/null
+++ b/projects/loft-photo-lite-3/mainPage.js
@@ -0,0 +1,35 @@
+import { doc } from 'prettier';
+import model from './model';
+
+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');
+
+ photoComp.style.backgrounImage = `url(${url})`;
+ headerPhotoComp.style.backgrounImage = `url('${frind.photo_50}')`;
+ photoNameComp.innerText = `${fiend.first_name ?? ''} ${frind.last_name ?? ''}`;
+ },
+
+ 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();
+ }
+ });
+ },
+};
diff --git a/projects/loft-photo-lite-3/model.js b/projects/loft-photo-lite-3/model.js
new file mode 100644
index 000000000..c6f3b2354
--- /dev/null
+++ b/projects/loft-photo-lite-3/model.js
@@ -0,0 +1,89 @@
+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();
+ },
+
+ 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 () {
+
+ },
+
+ 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;
+ },
+};
diff --git a/projects/loft-photo-lite-3/pages.js b/projects/loft-photo-lite-3/pages.js
new file mode 100644
index 000000000..7ed7899ab
--- /dev/null
+++ b/projects/loft-photo-lite-3/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-3/readme.md b/projects/loft-photo-lite-3/readme.md
new file mode 100644
index 000000000..f1dfa1834
--- /dev/null
+++ b/projects/loft-photo-lite-3/readme.md
@@ -0,0 +1,41 @@
+## Страница входа и листалка фотографий
+
+### Часть 1
+
+Возьмите за основу свой вариант `model.js` и `pages.js`, которые вы делали на предыдущих неделях.
+
+В [model.js](model.js) реализуйте:
+- возможность логиниться в ВК (в методе `login`)
+- возможность получать список друзей (в методе `init`)
+- возможность получать список фотографий друга (в методе `getFriendPhotos`)
+
+С этого момента, метод getNextPhoto модели должен опираться не на файлы `friends.json` и `photos.json`, а на данные из ВК
+
+> В методе init получите список друзей и сохраните их в какой-нибудь внешней переменной или в свойстве модели
+>
+> В методе getFriendPhotos уже реализована базовая логика кеширования фотографий, чтобы каждый раз заново не загружать фото друга, для которого вы уже получали список фото. Вам необходимо только написать код для получения самих фото из ВК
+
+Имейте в виду, что у каждой фотографии в ВК есть свойство `sizes`, которое содержит список одних и тех же изображений, но разного размера.
+Вам необходимы изображения с шириной не менее `360px`.
+Попробуйте вывести значения свойства `sizes` в консоль или воспользуйтесь отладчиком, чтобы посмотреть содержимое свойства.
+
+Страница входа находится в [loginPage.js](loginPage.js) и уже умеет вызывать нужные методы модели при нажатии на кнопку `Войти`.
+
+Страница входа открывается автоматически (см. файл [index.js](index.js)).
+
+### Часть 2
+
+В файле [mainPage.js](mainPage.js) содержится код главной страницы.
+
+Здесь вам необходимо реализовать листалку фотографий.
+Главная страницы уже содержит реализованный метод `getNextPhoto`, который вызывает метод `getNextPhoto` из модели и затем передает результат в метод `setFriendAndPhoto`.
+Его-то вам и нужно реализовать самостоятельно.
+Смысл метода `setFriendAndPhoto` в том, чтобы получить информацию о фото (`id` и `url`) и владельце фото и установить эту информацию в соответствующие элементы на странице.
+
+Далее реализуйте листалку фото.
+Листалка можно работать либо вверх-вниз, либо влево-вправо (выберите что вам большое нравится).
+Вы можете обработать как события `mousedown`/`mouseup`, так и события `touchstart`/`touchend`.
+
+> События touchstart/touchend будут работать только на touch-устройствах или в режиме эмуляции. Для включения режима эмуляции воспользуйтесь панелью DevTool в браузере.
+>
+> Все обработчики событий пишите в методе handleEvents
diff --git a/projects/loft-photo-lite-3/styles.css b/projects/loft-photo-lite-3/styles.css
new file mode 100644
index 000000000..62d5fba21
--- /dev/null
+++ b/projects/loft-photo-lite-3/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