diff --git a/projects/loft-photo/friends.json b/projects/loft-photo/friends.json
new file mode 100644
index 000000000..b861e0c00
--- /dev/null
+++ b/projects/loft-photo/friends.json
@@ -0,0 +1,32 @@
+[
+ {
+ "id": 0,
+ "avatar": "https://via.placeholder.com/100?text=avatar",
+ "firstName": "Adrian",
+ "lastName": "Norman"
+ },
+ {
+ "id": 1,
+ "avatar": "https://via.placeholder.com/100?text=avatar",
+ "firstName": "Gail",
+ "lastName": "Norton"
+ },
+ {
+ "id": 2,
+ "avatar": "https://via.placeholder.com/100?text=avatar",
+ "firstName": "Molina",
+ "lastName": "Rodgers"
+ },
+ {
+ "id": 3,
+ "avatar": "https://via.placeholder.com/100?text=avatar",
+ "firstName": "Adams",
+ "lastName": "Parrish"
+ },
+ {
+ "id": 4,
+ "avatar": "https://via.placeholder.com/100?text=avatar",
+ "firstName": "Mercer",
+ "lastName": "Wiggins"
+ }
+]
\ No newline at end of file
diff --git a/projects/loft-photo/images/arrow-left.svg b/projects/loft-photo/images/arrow-left.svg
new file mode 100644
index 000000000..a4e4c339a
--- /dev/null
+++ b/projects/loft-photo/images/arrow-left.svg
@@ -0,0 +1,4 @@
+
diff --git a/projects/loft-photo/images/button.svg b/projects/loft-photo/images/button.svg
new file mode 100644
index 000000000..6ce85ea9f
--- /dev/null
+++ b/projects/loft-photo/images/button.svg
@@ -0,0 +1,17 @@
+
diff --git a/projects/loft-photo/images/chat.svg b/projects/loft-photo/images/chat.svg
new file mode 100644
index 000000000..fc47d01e1
--- /dev/null
+++ b/projects/loft-photo/images/chat.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo/images/exit.svg b/projects/loft-photo/images/exit.svg
new file mode 100644
index 000000000..d28c122e1
--- /dev/null
+++ b/projects/loft-photo/images/exit.svg
@@ -0,0 +1,5 @@
+
diff --git a/projects/loft-photo/images/heart-red.svg b/projects/loft-photo/images/heart-red.svg
new file mode 100644
index 000000000..e9985dca6
--- /dev/null
+++ b/projects/loft-photo/images/heart-red.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo/images/heart.svg b/projects/loft-photo/images/heart.svg
new file mode 100644
index 000000000..4bcdacd80
--- /dev/null
+++ b/projects/loft-photo/images/heart.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo/images/logo.svg b/projects/loft-photo/images/logo.svg
new file mode 100644
index 000000000..12685673d
--- /dev/null
+++ b/projects/loft-photo/images/logo.svg
@@ -0,0 +1,11 @@
+
diff --git a/projects/loft-photo/images/send.svg b/projects/loft-photo/images/send.svg
new file mode 100644
index 000000000..5a55b025c
--- /dev/null
+++ b/projects/loft-photo/images/send.svg
@@ -0,0 +1,3 @@
+
diff --git a/projects/loft-photo/images/vert1.svg b/projects/loft-photo/images/vert1.svg
new file mode 100644
index 000000000..d5d86e658
--- /dev/null
+++ b/projects/loft-photo/images/vert1.svg
@@ -0,0 +1,22 @@
+
diff --git a/projects/loft-photo/images/vert2.svg b/projects/loft-photo/images/vert2.svg
new file mode 100644
index 000000000..0f5e75ed2
--- /dev/null
+++ b/projects/loft-photo/images/vert2.svg
@@ -0,0 +1,22 @@
+
diff --git a/projects/loft-photo/images/vert3.svg b/projects/loft-photo/images/vert3.svg
new file mode 100644
index 000000000..7b481af03
--- /dev/null
+++ b/projects/loft-photo/images/vert3.svg
@@ -0,0 +1,22 @@
+
diff --git a/projects/loft-photo/index.js b/projects/loft-photo/index.js
new file mode 100644
index 000000000..96a75fa10
--- /dev/null
+++ b/projects/loft-photo/index.js
@@ -0,0 +1,11 @@
+import pages from './pages';
+import('./styles.css');
+
+const pageNames = ['login', 'main', 'profile'];
+
+document.addEventListener('click', () => {
+ const pageName = getRandomElement(pageNames);
+
+ pages.openPage(pageName);
+});
+
diff --git a/projects/loft-photo/layout.html b/projects/loft-photo/layout.html
new file mode 100644
index 000000000..89845cfed
--- /dev/null
+++ b/projects/loft-photo/layout.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Loft Photo
+
+
+
+
+
+
+
+
+
+
+
Не пропустите лучшие моменты из жизни ваших друзей!
+
+
+
+
+
+
+
+
diff --git a/projects/loft-photo/mainPage.js b/projects/loft-photo/mainPage.js
new file mode 100644
index 000000000..be48fd412
--- /dev/null
+++ b/projects/loft-photo/mainPage.js
@@ -0,0 +1,126 @@
+import model from './model';
+import profilePage from './profilePage';
+import pages from './pages';
+
+export default {
+ async getNextPhoto() {
+ const { friend, id, url } = await model.getNextPhoto();
+ const photoStats = await model.photoStats(id);
+ this.setFriendAndPhoto(friend, id, url, photoStats);
+ },
+
+ setFriendAndPhoto(friend, id, url, stats) {
+ 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;
+ this.photoId = id;
+
+ 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}')`;
+ this.setLikes(stats.likes, stats.liked);
+ this.setComments(stats.comments);
+ },
+
+ 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');
+ });
+
+ document
+ .querySelector('.component-footer-container-social-likes')
+ .addEventListener('click', async () => {
+ const {likes, liked} = await model.like(this.photoId);
+ this.setLikes(likes, liked);
+ })
+
+ document
+ .querySelector('.component-footer-container-social-comments')
+ .addEventListener('click', async () => {
+ document.querySelector('.component-components').classList.remove('hidden');
+ await this.loadComments(this.photoId);
+ })
+
+ const input = document.querySelector('.component-comments-container-form-input');
+
+ document
+ .querySelector('.component-comments').addEventListener('click', (e) => {
+ if (e.target === e.currentTarget) {
+ document.querySelector('.component-comments').classList.add('hidden');
+ }
+ });
+
+ document
+ .querySelector('.component-comments-container-form-send')
+ .addEventListener('click', async () => {
+ if(input.ariaValueMax.trim().length) {
+ await model.postComment(this.photoId, input.value.trim());
+ input.value = '';
+ await this.loadComments(this.photoId);
+ }
+ })
+
+ },
+
+ async loadComments(photo) {
+ const comments = await model.getComments(photo);
+ const commentsElements = commentsTemplate({
+ list: comments.map((comment) => {
+ return {
+ name: `${comment.user.first_name ?? ''} ${comment.user.last_name ?? ''}`,
+ photo: comment.user.photo_50,
+ text: comment.text,
+ }
+ })
+ })
+
+ document.querySelector('.component-comments-container-list').innerHTML = '';
+ document.querySelector('.component-comments-container-list').append(commentsElements);
+ this.setComments(comments.length);
+ },
+
+ setLikes(total, liked) {
+ const likesElement = document.querySelector('.component-footerj-container-social-likes');
+
+ likesElement.innerText = total;
+
+ if(liked) {
+ likesElement.classList.add('liked');
+ } else {
+ likesElement.classList.remove('liked');
+ }
+ },
+
+ setComments(total) {
+ const likesElement = document.querySelector('.component-footer-container-social-comments');
+ likesElement.innerHTML = total;
+ },
+};
diff --git a/projects/loft-photo/model.js b/projects/loft-photo/model.js
new file mode 100644
index 000000000..c039ee7c4
--- /dev/null
+++ b/projects/loft-photo/model.js
@@ -0,0 +1,151 @@
+// eslint-disable-next-line no-unused-vars
+import photosDB from './photos.json';
+// eslint-disable-next-line no-unused-vars
+import friendsDB from './friends.json';
+
+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) {
+ this.token = response.session.sid;
+ 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);
+ },
+
+async callServer(method, queryParams, body) {
+ queryParams = {
+ ...queryParams,
+ method,
+ };
+
+ const query = Object.entries(queryParams)
+ .reduce((all, [name, value]) => {
+ all.push(`${name}=${encodeURIComponent(value)}`);
+ return all;
+ }, [])
+ .join('&');
+ const params = {
+ headers: {
+ vk_token: this.token,
+ },
+ };
+
+ if (body) {
+ params.method = 'POST';
+ params.body = JSON.stringify(body);
+ }
+
+ // const response = await fetch (``)
+},
+
+ async like(photo) {
+ return this.callServer('like', {photo});
+ },
+ async photoStats(photo) {
+ return this.callServer('photoStats', {photo});
+ },
+ async postComment(photo, text) {
+ return this.callServer('postComments', {photo}, {text})
+ },
+ async getComments(photo) {
+ return this.callServer('getComments', {photo});
+ },
+
+};
diff --git a/projects/loft-photo/pages.js b/projects/loft-photo/pages.js
new file mode 100644
index 000000000..fb923c498
--- /dev/null
+++ b/projects/loft-photo/pages.js
@@ -0,0 +1,18 @@
+const pagesMap = {
+ login: '.page-login',
+ main: '.page-main',
+ profile: '.page-profile',
+};
+
+let currentPage = null;
+
+export default {
+ openPage(name) {
+ const selector = pagesMap[name];
+ const element = document.querySelector(selector);
+
+ currentPage?.classList.add('.hidden');
+ currentPage = element;
+ currentPage.classList.remove('.hidden');
+ },
+};
diff --git a/projects/loft-photo/photos.json b/projects/loft-photo/photos.json
new file mode 100644
index 000000000..a299e6057
--- /dev/null
+++ b/projects/loft-photo/photos.json
@@ -0,0 +1,72 @@
+{
+ "0": [
+ {
+ "id": 10,
+ "url": "https://via.placeholder.com/360x680?text=photo 1 for Adrian Norman"
+ },
+ {
+ "id": 11,
+ "url": "https://via.placeholder.com/360x680?text=photo 2 for Adrian Norman"
+ },
+ {
+ "id": 12,
+ "url": "https://via.placeholder.com/360x680?text=photo 3 for Adrian Norman"
+ }
+ ],
+ "1": [
+ {
+ "id": 20,
+ "url": "https://via.placeholder.com/360x680?text=photo 1 for Gail Norton"
+ },
+ {
+ "id": 21,
+ "url": "https://via.placeholder.com/360x680?text=photo 2 for Gail Norton"
+ },
+ {
+ "id": 22,
+ "url": "https://via.placeholder.com/360x680?text=photo 3 for Gail Norton"
+ }
+ ],
+ "2": [
+ {
+ "id": 30,
+ "url": "https://via.placeholder.com/360x680?text=photo 1 for Molina Rodgers"
+ },
+ {
+ "id": 31,
+ "url": "https://via.placeholder.com/360x680?text=photo 2 for Molina Rodgers"
+ },
+ {
+ "id": 32,
+ "url": "https://via.placeholder.com/360x680?text=photo 3 for Molina Rodgers"
+ }
+ ],
+ "3": [
+ {
+ "id": 40,
+ "url": "https://via.placeholder.com/360x680?text=photo 1 for Adams Parrish"
+ },
+ {
+ "id": 41,
+ "url": "https://via.placeholder.com/360x680?text=photo 2 for Adams Parrish"
+ },
+ {
+ "id": 42,
+ "url": "https://via.placeholder.com/360x680?text=photo 3 for Adams Parrish"
+ }
+ ],
+ "4": [
+ {
+ "id": 50,
+ "url": "https://via.placeholder.com/360x680?text=photo 1 for Mercer Wiggins"
+ },
+ {
+ "id": 51,
+ "url": "https://via.placeholder.com/360x680?text=photo 2 for Mercer Wiggins"
+ },
+ {
+ "id": 52,
+ "url": "https://via.placeholder.com/360x680?text=photo 3 for Mercer Wiggins"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/projects/loft-photo/server/index.js b/projects/loft-photo/server/index.js
new file mode 100644
index 000000000..187a64747
--- /dev/null
+++ b/projects/loft-photo/server/index.js
@@ -0,0 +1,124 @@
+const http = require('node:http');
+const https = require('node:https');
+const url = require('node:url');
+
+const DB = {
+ tokens: new Map(),
+ likes: new Map(),
+ comments: new Map(),
+};
+
+const methods = {
+ like(req, res, url, vkUser) {
+ const photoId = url.searchParams.get('photo');
+ let photoLikes = DB.likes.get(photoId);
+
+ if (!photoLikes) {
+ photoLikes = new Map();
+ DB.likes.set(photoId, photoLikes);
+ }
+
+ if (photoLikes.get(vkUser.id)) {
+ photoLikes.delete(vkUser.id);
+ return { likes: photoLikes.size, liked: false };
+ }
+
+ photoLikes.set(vkUser.id, true);
+ return { likes: photoLikes.size, liked: true }
+ },
+ photoStats(req, res, url, vkUser) {
+ const photoId = url.searchParams.get('photo');
+ const photoLikes = DB.likes.get(photoId);
+ const photoComments = DB.comments.get(photoId);
+
+ return {
+ likes: photoLikes?.size ?? 0,
+ liked: photoLikes?.has(vkUser.id) ?? false,
+ comments: photoComments?.length ?? 0,
+ };
+ },
+ postComment(req, res, url, vkUser, body) {
+ const photoId = url.searchParams.get('photo');
+ let photoComments = DB.comments.get(photoId);
+
+ if (!photoComments) {
+ photoComments = [];
+ DB.comments.set(photoId, photoComments);
+ }
+
+ photoComments.unshift({ user: vkUser, text: body.text });
+ },
+ getComments(req, res, url) {
+ const photoId = url.searchParams.get('photo');
+ return DB.comments.get(photoId) ?? [];
+ },
+};
+
+http
+ .createServer(async (req, res) => {
+ console.log('➡️ Поступил запрос:', req.method, req.url);
+ const token = req.headers['vk_token'];
+ const parsed = new url.URL(req.url, 'http://localhost');
+ const vkUser = await getMe(token);
+ const body = await readBody(req);
+ const method = parsed.searchParams.get('method');
+ const responseData = await methods[method]?.(req, res, parsed, vkUser, body);
+
+ res.end(JSON.stringify(responseData ?? null));
+ })
+ .listen('8888', () => {
+ console.log('🚀 Сервер запущен');
+ });
+
+async function readBody(req) {
+ if (req.method === 'GET') {
+ return null;
+ }
+
+ return new Promise((resolve) => {
+ let body = '';
+ req
+ .on('data', (chunk) => {
+ body += chunk;
+ })
+ .on('end', () => resolve(JSON.parse(body)));
+ });
+}
+
+async function getVKUser(token) {
+ const body = await new Promise((resolve, reject) =>
+ https
+ .get(
+ `https://api.vk.com/method/users.get?access_token=${token}&fields=photo_50&v=5.120`
+ )
+ .on('response', (res) => {
+ let body = '';
+
+ res.setEncoding('utf8');
+ res
+ .on('data', (chunk) => {
+ body += chunk;
+ })
+ .on('end', () => resolve(JSON.parse(body)));
+ })
+ .on('error', reject)
+ );
+
+ return body.response[0];
+}
+
+async function getMe(token) {
+ const existing = DB.tokens.get(token);
+
+ if (existing) {
+ return existing;
+ }
+
+ const user = getVKUser(token);
+
+ DB.tokens.set(token, user);
+
+ return user;
+}
+
+
diff --git a/projects/loft-photo/settings.json b/projects/loft-photo/settings.json
new file mode 100644
index 000000000..3d20b4405
--- /dev/null
+++ b/projects/loft-photo/settings.json
@@ -0,0 +1,7 @@
+{
+ "proxy": {
+ "/loft-photo/api/": {
+ "target": "http://localhost:8888"
+ }
+ }
+}
diff --git a/projects/loft-photo/styles.css b/projects/loft-photo/styles.css
new file mode 100644
index 000000000..62d5fba21
--- /dev/null
+++ b/projects/loft-photo/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