diff --git a/projects/gp-3/friendsFilter.js b/projects/gp-3/friendsFilter.js new file mode 100644 index 0000000..4b58a3b --- /dev/null +++ b/projects/gp-3/friendsFilter.js @@ -0,0 +1,173 @@ +import VKAPI from './vkAPI'; +import FriendsList from './friendsList'; +import { LocalStorage, VKStorage } from './storage'; + +export default class FriendsFilter { + constructor() { + this.lsKey = 'LS_FRIENDS_FILTER'; + this.allFriendsDOMFilter = document.querySelector( + '[data-role=filter-input][data-list=all]' + ); + this.allFriendsDOMList = document.querySelector( + '[data-role=list-items][data-list=all]' + ); + this.bestFriendsDOMFilter = document.querySelector( + '[data-role=filter-input][data-list=best]' + ); + this.bestFriendsDOMList = document.querySelector( + '[data-role=list-items][data-list=best]' + ); + + this.api = new VKAPI(6789124, 2); + this.allFriends = new FriendsList(new VKStorage(this.api)); + this.bestFriends = new FriendsList(new LocalStorage(this.api, this.lsKey)); + + this.init(); + } + + async init() { + await this.api.init(); + await this.api.login(); + await this.allFriends.load(); + await this.bestFriends.load(); + + for (const item of this.bestFriends.valuesIterable()) { + await this.allFriends.delete(item.id); + } + + this.reloadList(this.allFriendsDOMList, this.allFriends); + this.reloadList(this.bestFriendsDOMList, this.bestFriends); + + document.addEventListener('mousedown', this.onMouseDown.bind(this)); + document.addEventListener('mousemove', this.onMouseMove.bind(this)); + document.addEventListener('mouseup', this.onMouseUp.bind(this)); + document.addEventListener('click', this.onClick.bind(this)); + + this.allFriendsDOMFilter.addEventListener('input', (e) => { + this.allFriendsFilter = e.target.value; + this.reloadList(this.allFriendsDOMList, this.allFriends, this.allFriendsFilter); + }); + this.bestFriendsDOMFilter.addEventListener('input', (e) => { + this.bestFriendsFilter = e.target.value; + this.reloadList(this.bestFriendsDOMList, this.bestFriends, this.bestFriendsFilter); + }); + } + + isMatchingFilter(source, filter) { + return source.toLowerCase().includes(filter.toLowerCase()); + } + + reloadList(listDOM, friendsList, filter) { + const fragment = document.createDocumentFragment(); + + listDOM.innerHTML = ''; + + for (const friend of friendsList.valuesIterable()) { + const fullName = `${friend.first_name} ${friend.last_name}`; + + if (!filter || this.isMatchingFilter(fullName, filter)) { + const friendDOM = this.createFriendDOM(friend); + fragment.append(friendDOM); + } + } + + listDOM.append(fragment); + } + + createFriendDOM(data) { + const root = document.createElement('div'); + + root.dataset.role = 'list-item'; + root.dataset.friendId = data.id; + root.classList.add('list-item'); + root.innerHTML = ` + +
${data.first_name} ${data.last_name}
+
+ `; + + return root; + } + + move(friendId, from, to) { + if (from === 'all' && to === 'best') { + const friend = this.allFriends.delete(friendId); + this.bestFriends.add(friend); + } else if (from === 'best' && to === 'all') { + const friend = this.bestFriends.delete(friendId); + this.allFriends.add(friend); + } + + this.bestFriends.save(); + this.reloadList(this.allFriendsDOMList, this.allFriends, this.allFriendsFilter); + this.reloadList(this.bestFriendsDOMList, this.bestFriends, this.bestFriendsFilter); + } + + onMouseDown(e) { + const sourceItem = e.target.closest('[data-role=list-item]'); + + if (!sourceItem) { + return; + } + + const friendId = sourceItem.dataset.friendId; + const sourceList = e.target.closest('[data-role=list-items]').dataset.list; + + this.dragging = { + offsetX: e.offsetX, + offsetY: e.offsetY, + sourceItem, + friendId, + sourceList, + pending: true, + }; + } + + onMouseMove(e) { + if (!this.dragging) { + return; + } + + e.preventDefault(); + + if (this.dragging.pending) { + const rect = this.dragging.sourceItem.getBoundingClientRect(); + const clone = this.dragging.sourceItem.cloneNode(true); + clone.classList.add('list-item-clone'); + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + document.body.append(clone); + this.dragging.pending = false; + this.dragging.clone = clone; + } + + this.dragging.clone.style.left = `${e.clientX - this.dragging.offsetX}px`; + this.dragging.clone.style.top = `${e.clientY - this.dragging.offsetY}px`; + } + + onMouseUp(e) { + if (!this.dragging || this.dragging.pending) { + this.dragging = null; + return; + } + + const targetList = e.target.closest('[data-role=list-items]'); + + if (targetList) { + const moveTo = targetList.dataset.list; + this.move(this.dragging.friendId, this.dragging.sourceList, moveTo); + } + + this.dragging.clone.remove(); + this.dragging = null; + } + + onClick(e) { + if (e.target.dataset.role === 'list-item-swap') { + const sourceList = e.target.closest('[data-role=list-items]').dataset.list; + const friendId = e.target.dataset.friendId; + + this.move(friendId, sourceList, sourceList === 'all' ? 'best' : 'all'); + } + } +} diff --git a/projects/gp-3/friendsList.js b/projects/gp-3/friendsList.js new file mode 100644 index 0000000..849fb24 --- /dev/null +++ b/projects/gp-3/friendsList.js @@ -0,0 +1,40 @@ +export default class FriendsList { + constructor(storage) { + this.storage = storage; + this.data = new Map(); + } + + async load() { + this.data.clear(); + const list = await this.storage.load(); + + for (const item of list) { + this.add(item); + } + } + + save() { + this.storage.save([...this.data.values()]); + } + + add(friend) { + if (!this.data.has(friend.id)) { + this.data.set(friend.id, friend); + } + } + + delete(friendId) { + friendId = Number(friendId); + const friend = this.data.get(friendId); + + if (friend) { + this.data.delete(friendId); + } + + return friend; + } + + valuesIterable() { + return this.data.values(); + } +} diff --git a/projects/gp-3/index.html b/projects/gp-3/index.html new file mode 100644 index 0000000..302e344 --- /dev/null +++ b/projects/gp-3/index.html @@ -0,0 +1,131 @@ + + +
+
+
+
+
+ +
+
+ Ваши друзья +
+
+
+
+
+ +
+
+ Лучшие друзья +
+
+
+
+
+
diff --git a/projects/gp-3/index.js b/projects/gp-3/index.js new file mode 100644 index 0000000..ea2d9a8 --- /dev/null +++ b/projects/gp-3/index.js @@ -0,0 +1,7 @@ +import './index.html'; +import FriendsFilter from './friendsFilter'; + +new FriendsFilter( + document.querySelector('.list.all-friends'), + document.querySelector('.list.best-friends') +); diff --git a/projects/gp-3/storage.js b/projects/gp-3/storage.js new file mode 100644 index 0000000..c1e453f --- /dev/null +++ b/projects/gp-3/storage.js @@ -0,0 +1,30 @@ +export class LocalStorage { + constructor(api, key) { + this.api = api; + this.key = key; + } + + async load() { + const friendsIds = JSON.parse(localStorage.getItem(this.key) || '[]'); + return await this.api.getUsers(friendsIds); + } + + save(data) { + localStorage.setItem(this.key, JSON.stringify(data.map((item) => item.id))); + } +} + +export class VKStorage { + constructor(api) { + this.api = api; + } + + async load() { + const response = await this.api.getFriends(); + return response.items; + } + + save() { + throw new Error('not supported'); + } +} diff --git a/projects/gp-3/vkAPI.js b/projects/gp-3/vkAPI.js new file mode 100644 index 0000000..c932add --- /dev/null +++ b/projects/gp-3/vkAPI.js @@ -0,0 +1,64 @@ +/* global VK */ + +export default class VKAPI { + constructor(appId, perms) { + this.appId = appId; + this.perms = perms; + } + + init() { + return new Promise((resolve) => { + const script = document.createElement('script'); + script.src = 'http://vk.com/js/api/openapi.js'; + document.body.appendChild(script); + script.addEventListener('load', resolve); + }); + } + + login() { + return new Promise((resolve, reject) => { + VK.init({ + apiId: this.appId, + }); + + VK.Auth.login((response) => { + if (response.session) { + resolve(response); + } else { + reject(new Error('Не удалось авторизоваться')); + } + }, this.perms); + }); + } + + 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); + } + }); + }); + } + + getFriends() { + const params = { + fields: ['photo_50'], + }; + + return this.callApi('friends.get', params); + } + + getUsers(ids) { + const params = { + fields: ['photo_50'], + user_ids: ids, + }; + + return this.callApi('users.get', params); + } +}