Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions projects/gp-3/friendsFilter.js
Original file line number Diff line number Diff line change
@@ -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 = `
<img class="list-item-photo" src="${data.photo_50}"/>
<div class="list-item-name">${data.first_name} ${data.last_name}</div>
<div class="list-item-swap" data-role="list-item-swap" data-friend-id="${data.id}"></div>
`;

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');
}
}
}
40 changes: 40 additions & 0 deletions projects/gp-3/friendsList.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
131 changes: 131 additions & 0 deletions projects/gp-3/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<style>
html, body, #app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}

#app {
display: flex;
justify-content: center;
}

.container {
border: 2px solid orange;
border-radius: 5px;
position: relative;
top: 10vh;
min-width: 40vw;
width: 50vw;
height: 500px;
}

.lists {
display: flex;
height: 100%;
}

.list {
flex-grow: 1;
flex-basis: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
padding: 0 10px;
}

.list-filter {
padding: 10px 0;
}

.list-filter input {
width: 100%;
height: 30px;
box-sizing: border-box;
}

.list-items {
flex-grow: 1;
overflow: scroll;
}

.list-header {
font-weight: bold;
font-size: 14px;
}

.list-item {
display: flex;
align-items: center;
margin: 5px 0;
}

.list-item-clone {
background: #ffe6c8;
pointer-events: none;
position: absolute;
}

.list-item:hover {
background: #ffe6c8;
cursor: default;
}

.list-item-photo {
border-radius: 50%;
width: 50px;
height: 50px;
}

.list-item-name {
flex-grow: 1;
padding: 0 15px;
}

.list-item-swap,
.list-item-swap:hover,
.list-item-swap:active,
.list-item-swap:visited {
text-decoration: none;
color: black;
}

.all-friends .list-item-swap:after {
display: block;
content: '>';
cursor: pointer;
}

.best-friends .list-item-swap:after {
display: block;
content: '<';
cursor: pointer;
}
</style>

<div id="app">
<div class="container">
<div class="lists">
<div class="list all-friends">
<div class="list-filter">
<input type="text" data-role="filter-input" data-list="all" placeholder="фильтр">
</div>
<div class="list-header">
Ваши друзья
</div>
<div class="list-items" data-role="list-items" data-list="all"></div>
</div>
<div class="list best-friends">
<div class="list-filter">
<input type="text" data-role="filter-input" data-list="best" placeholder="фильтр">
</div>
<div class="list-header">
Лучшие друзья
</div>
<div class="list-items" data-role="list-items" data-list="best"></div>
</div>
</div>
</div>
</div>
7 changes: 7 additions & 0 deletions projects/gp-3/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import './index.html';
import FriendsFilter from './friendsFilter';

new FriendsFilter(
document.querySelector('.list.all-friends'),
document.querySelector('.list.best-friends')
);
30 changes: 30 additions & 0 deletions projects/gp-3/storage.js
Original file line number Diff line number Diff line change
@@ -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');
}
}
Loading