Список пользователей

1
Админ
Постов: 232
2
VIP
Постов: 72
3
Элита
Постов: 51
4
Проверенные
Постов: 37
5
VIP
Постов: 35
6
Проверенные
Постов: 32
7
Пользователи
Постов: 31
8
Проверенные
Постов: 29

  • Страница 1 из 1
  • 1
Модуль ВК «Блок друзей» для Ucoz v 17.1 Эффекты
Дата: Четверг, 02.04.2026, 17:07 | Сообщение # 1 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:232
         Регистрация:20.10.2016

Продолжаем совершенствовать модуль «Друзья». На этот раз добавлена функция всплывающего окна для отправки подарков.

Также реализованы новые визуальные эффекты при открытии и закрытии окна: оно появляется с плавной анимацией, а при закрытии распадается на «осколки».

Изначально рассматривался более зрелищный вариант с эффектом взрыва при закрытии окна и при удалении друзей. Однако в процессе тестирования стало понятно, что такие эффекты выглядят слишком агрессивно и со временем начинают раздражать. Поэтому я вернулся к более спокойному варианту оформления: мягкому, светлому и ненавязчивому, без резких визуальных акцентов.

На первый взгляд яркие эффекты могут показаться интересными, но при длительном использовании интерфейса они перегружают восприятие.

Ниже будет видео, где всё можно посмотреть наглядно.



Исходной код

Код
<!-- MINI BLOCK ДРУЗЬЯ -->
<div class="vk-old-block">
<div class="vk-old-header">Друзья</div>
<div class="vk-old-footer" id="friends-count">0 друзей</div>

<div class="vk-old-content">
<div class="vk-friends"></div>
</div>

<div class="vk-old-footer" id="friends-footer" title="Показать всех друзей">
Показать всех друзей →
</div>
</div>

<link rel="stylesheet" href="/css/friends04.css" media="all">

<div id="uc-online-list" style="display:none;">
$ONLINE_USERS_LIST$
</div>

<script>
(function(){
const USER_ID = window.UCOZ_DATA?.userId || "0";
if(USER_ID === "0") return;

const $container = $('.vk-friends');
const $counter = $('#friends-count');
const $footer = $('#friends-footer');
const DEFAULT_AVA = '/.s/src/profile/img/profile_photo_thumbnail.png';
const LS_FRIENDS = 'friends_' + USER_ID;
let allFriends = [];

/* =========================
РЕНДЕР МИНИ-БЛОКА
========================= */
function renderMini(friends){
const onlineHtml = $('#uc-online-list').text().toLowerCase();
if(!friends || friends.length === 0){
const empty = document.createElement('div');
empty.className = 'vk-empty-wrap';
empty.innerHTML = `
<div class="vk-empty-icon">
<svg width="58" height="58" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 11c1.66 0 3-1.34 3-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zM8 11c1.66 0 3-1.34 3-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3z" fill="#99a2ad"/>
<path d="M8 13c-2.67 0-8 1.34-8 4v2h10v-2c0-1.2.6-2.3 1.6-3.2C10.3 13.3 9 13 8 13zm8 0c-.9 0-2.3.2-3.6.8 1 .9 1.6 2 1.6 3.2v2h10v-2c0-2.66-5.33-4-8-4z" fill="#99a2ad"/>
</svg>
</div>
<div class="vk-empty-title">У вас нет друзей</div>
<div class="vk-empty-sub">Добавляйте людей, чтобы видеть их здесь</div>
<div class="vk-empty-action">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Добавить друзей
</div>
`;
empty.querySelector('.vk-empty-action').onclick = () => location.href = '/index/15-2';
$container[0].replaceChildren(empty);
$counter.html('0 друзей');
return;
}

$container.empty();
friends.slice(0, 6).forEach(f => {
const isOnline = onlineHtml.includes(f.nick.toLowerCase());
const statusDot = `<span class="statusDot ${isOnline ? 'onlineDot' : 'offlineDot'}"></span>`;
const card = $(`
<div class="vk-friend" title="${f.nick}">
<img src="${f.ava}">
<span>${f.nick}</span>
${statusDot}
</div>
`);
card.on('click', () => location.href = f.profile);
$container.append(card);
});
$counter.html(`${friends.length} друзей`);
}

/* =========================
POPUP
========================= */
function showPopup(){
if(!allFriends.length) return;
$('.vk-ui-overlay').remove();
let visibleCount = 6;
let isLoadingMore = false;
let lastFilter = '';
const overlay = $('<div class="vk-ui-overlay"></div>');
const popup = $(`
<div class="vk-ui-popup">
<div class="vk-ui-header">
<div class="vk-ui-title">
Друзья <span class="vk-ui-title-count">${allFriends.length}</span>
</div>
<div class="vk-ui-search">
<input type="text" placeholder="Поиск друзей" id="friend-search">
</div>
<button class="vk-ui-close">×</button>
</div>
<div class="vk-ui-list"></div>
<div class="vk-ui-more-wrap"></div>
</div>
`);
const list = popup.find('.vk-ui-list');
const moreWrap = popup.find('.vk-ui-more-wrap');
const onlineHtml = $('#uc-online-list').text().toLowerCase();
function getStatus(f){
let badge = 'Без статуса';
let cls = 'nostatus';
let letter = '';
const g = (f.group || '').toLowerCase();
if (g.includes('кумир')) { badge = 'Кумир'; cls = 'idol'; letter = 'К'; }
else if (g.includes('друг')) { badge = 'Друг'; cls = 'friend'; letter = 'Д'; }
else if (g.includes('сем')) { badge = 'Семья'; cls = 'family'; letter = 'С'; }
else if (g.includes('знаком')) { badge = 'Знакомый'; cls = 'acquaintance'; letter = 'З'; }
else if (g.includes('прият')) { badge = 'Приятель'; cls = 'buddy'; letter = 'П'; }
else if (g.includes('коллег')) { badge = 'Коллега'; cls = 'colleague'; letter = 'К'; }
return { badge, cls, letter };
}
function renderList(filter = ''){
list.empty();
moreWrap.empty();
lastFilter = filter;
const filtered = allFriends.filter(f =>
(f.nick || '').toLowerCase().includes(filter.toLowerCase())
);
const slice = filtered.slice(0, visibleCount);
slice.forEach(f => {
const { badge, cls, letter } = getStatus(f);
const isOnline = onlineHtml.includes((f.nick || '').toLowerCase());
const row = $(`
<div class="vk-ui-row ${cls}">
<div class="vk-ui-ava">
<img src="${f.ava}">
${letter ? `
<span class="vk-ui-letter ${cls}">${letter}</span>
` : ''}
<span class="statusDot ${isOnline ? 'onlineDot' : 'offlineDot'}"></span>
</div>
<div class="vk-ui-info">
<div class="vk-ui-name">${f.nick}</div>
<div class="vk-ui-badge ${cls}">${badge}</div>
</div>
    <div class="vk-ui-actions">
<button class="vk-gift-btn">
    <svg class="gift-icon" viewBox="0 0 24 24">
        <defs>
            <linearGradient id="giftGradHover" x1="0%" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" stop-color="#667eea"/>
                <stop offset="100%" stop-color="#764ba2"/>
            </linearGradient>
        </defs>
        <path class="gift-shape"
              d="M20 7h-3c.1-.3.2-.6.2-1 0-1.7-1.3-3-3-3-1.1 0-2.1.6-2.6 1.5C10.9 3.6 9.9 3 8.8 3 7.1 3 5.8 4.3 5.8 6c0 .4.1.7.2 1H3v4h17V7z"/>
        <path class="gift-shape"
              d="M4 11v9c0 .6.4 1 1 1h6v-10H4zm9 10h6c.6 0 1-.4 1-1v-9h-7v10z"/>
    </svg>
</button>
        <button class="vk-del-btn">✕</button>
        <div class="vk-ui-arrow">›</div>
    </div>
</div>
`);
// =========================
// ПЕРЕХОД В ПРОФИЛЬ
// =========================
row.on('click', function(e){
    if(!$(e.target).closest('.vk-del-btn, .vk-gift-btn').length){
        location.href = f.profile;
    }
});
// =========================
// ПОДАРОК
// =========================
row.find('.vk-gift-btn').on('click', function(e){
    e.stopImmediatePropagation();
    if(!f.profile) return;
    const idMatch =
        f.profile.match(/-(\d+)(?:-|$)/) ||
        f.profile.match(/\/(\d+)$/);
    if(!idMatch) return alert('Не удалось определить ID');
    const targetId = idMatch[1];
    if(String(targetId) === String(USER_ID)){
        return alert('Самому себе нельзя 😄');
    }
    $('.vk-ui-overlay').fadeOut(150, function() {
        $(this).remove();
    });
    new _uWnd(
        'giftWnd',
        'Вручить подарок',
        380,
        200,
        {maxh:300,minh:100,closeonesc:1},
        {url:'/index/55-' + targetId}
    );
});
// =========================
// УДАЛЕНИЕ ДРУГА
// =========================
row.find('.vk-del-btn').on('click', function (e) {
    e.stopImmediatePropagation();

    const btn = $(this);

    if (btn.data('loading')) return;
    if (!f.del) return alert('Нет ссылки удаления');

    openDeleteModal(f.nick, f.ava, () => {

        btn.data('loading', true);

        // =========================
        // блок кнопки
        // =========================
        btn.text('...').css({
            opacity: 0.5,
            pointerEvents: 'none'
        });

        $.get(f.del).done(() => {

            // =========================
            // REMOVE DATA
            // =========================
            allFriends = allFriends.filter(x => x.nick !== f.nick);

            // =========================
            // FX CENTER
            // =========================
            const rect = row[0].getBoundingClientRect();
            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;

            // =========================
            // 1. лёгкий "хруст"
            // =========================
            row.css({
                transition: '0.12s cubic-bezier(.2,1,.2,1)',
                transform: 'translateX(14px) scale(1.03)'
            });

            // =========================
            // 2. плавный развал (НЕ взрыв)
            // =========================
            setTimeout(() => {

                row.css({
                    transition: '0.55s cubic-bezier(.1,.9,.2,1)',
                    transform: `
                        translateX(420px)
                        translateY(-80px)
                        rotate(12deg)
                        scale(0.3)
                    `,
                    opacity: 0,
                    filter: 'blur(6px)'
                });

                // =========================
                // ICE SHARDS (РАЗВАЛ, НЕ ВЗРЫВ)
                // =========================
                const shardsCount = 42;

                for (let i = 0; i < shardsCount; i++) {

                    const shard = document.createElement('div');

                    const rand = Math.random();

                    // больше крупных кусков
                    if (rand > 0.55) {
                        shard.className = 'vk-ice-shard big';
                    } else if (rand > 0.25) {
                        shard.className = 'vk-ice-shard medium';
                    } else {
                        shard.className = 'vk-ice-shard small';
                    }

                    const angle = Math.random() * Math.PI * 2;

                    // =========================
                    // МЯГКОЕ "ОСЫПАНИЕ"
                    // =========================
                    let distance;

                    if (shard.classList.contains('big')) {
                        distance = 90 + Math.random() * 140;   // крупные "отваливаются"
                    } else if (shard.classList.contains('medium')) {
                        distance = 60 + Math.random() * 120;
                    } else {
                        distance = 30 + Math.random() * 80;
                    }

                    shard.style.left = centerX + 'px';
                    shard.style.top = centerY + 'px';

                    shard.style.setProperty('--dx', Math.cos(angle) * distance + 'px');
                    shard.style.setProperty('--dy', Math.sin(angle) * distance + 'px');

                    document.body.appendChild(shard);

                    setTimeout(() => shard.remove(), 1400);
                }

                // =========================
                //  мягкая вспышка
                // =========================
                const flash = document.createElement('div');
                flash.style.position = 'fixed';
                flash.style.inset = '0';
                flash.style.zIndex = 999998;
                flash.style.pointerEvents = 'none';
                flash.style.background =
                    'radial-gradient(circle, rgba(190,240,255,0.75), transparent 70%)';
                flash.style.animation = 'vkFlashFire 0.35s ease-out forwards';

                document.body.appendChild(flash);
                setTimeout(() => flash.remove(), 350);

            }, 60);

            // =========================
            // CLEANUP
            // =========================
            setTimeout(() => {

                row.remove();

                renderMini(allFriends);
                $counter.html(`${allFriends.length} друзей`);

                localStorage.setItem(LS_FRIENDS, JSON.stringify(allFriends));
                renderList(lastFilter);

            }, 550);

        }).always(() => {
            btn.data('loading', false);
        });

    });
});

// =========================
// ДОБАВЛЕНИЕ В СПИСОК
// =========================
list.append(row);
});
// =========================
// КНОПКА "ПОКАЗАТЬ ЕЩЁ"
// =========================
if (filtered.length > visibleCount) {
const btn = $(`
<div class="vk-more-btn">
<span class="vk-more-text">Show more </span>
</div>
`);
btn.on('click', function(){
if(isLoadingMore) return;
isLoadingMore = true;
const $btn = $(this);
$btn.addClass('loading');
$btn.html(`
<span class="vk-spinner"></span>
<span>Загрузка...</span>
`);
setTimeout(() => {
visibleCount += 6;
isLoadingMore = false;
renderList(lastFilter);
}, 450);
});
moreWrap.append(btn);
}
}
renderList();
// поиск
popup.find('#friend-search').on('input', function(){
visibleCount = 6;
renderList($(this).val());
});

// ========================
// ПЛАВНОЕ ПОЯВЛЕНИЕ КАК ОБЛАЧКО (улучшенная версия)
// ========================
overlay.css({
     position: 'fixed',
     inset: 0,
     zIndex: 2147483647,
     opacity: 0,
     transition: 'opacity 0.3s ease'
});

popup.css({
     position: 'relative',
     zIndex: 2147483647,
     opacity: 0,
     transform: 'scale(0.75) translateY(80px)',
     transition: 'all 1.45s cubic-bezier(0.25, 0.8, 0.25, 1)'   // очень плавная и мягкая анимация
});

overlay.append(popup);
$('body').append(overlay);

// Запуск анимации появления
setTimeout(() => {
     overlay.css('opacity', '1');
     popup.css({
         opacity: '1',
         transform: 'scale(1) translateY(0)'
     });
}, 10);

// Плавное закрытие
const closePopup = () => {

    try {
        const rect = popup[0].getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        // =========================
        //  ICE FLASH (мягкий)
        // =========================
        const flash = document.createElement('div');
        flash.style.position = 'fixed';
        flash.style.inset = '0';
        flash.style.zIndex = 999999;
        flash.style.pointerEvents = 'none';
        flash.style.background = 'radial-gradient(circle, rgba(200,245,255,0.85), transparent 75%)';
        flash.style.animation = 'vkFlashFire 0.3s ease-out forwards';
        document.body.appendChild(flash);
        setTimeout(() => flash.remove(), 300);

        // =========================
        //  SHOCKWAVE (очень мягкая)
        // =========================
        const wave = document.createElement('div');
        wave.className = 'vk-wave';
        wave.style.left = (centerX - 60) + 'px';
        wave.style.top = (centerY - 60) + 'px';
        wave.style.opacity = '0.5';
        document.body.appendChild(wave);
        setTimeout(() => wave.remove(), 700);

        // =========================
        //  ICE SHARDS (РАСЫПАНИЕ, НЕ ВЗРЫВ)
        // =========================
        const shardsCount = 55; //  больше осколков

        for (let i = 0; i < shardsCount; i++) {

            const shard = document.createElement('div');

            const rand = Math.random();

            // =========================
            //  БОЛЬШЕ КРУПНЫХ КУСКОВ
            // =========================
            if (rand > 0.6) {
                shard.className = 'vk-ice-shard big';
            } else if (rand > 0.25) {
                shard.className = 'vk-ice-shard medium';
            } else {
                shard.className = 'vk-ice-shard small';
            }

            const angle = Math.random() * Math.PI * 2;

            // =========================
            //  МЯГКОЕ РАСЫПАНИЕ (без взрыва)
            // =========================
            let distance;

            if (shard.classList.contains('big')) {
                distance = 120 + Math.random() * 180;   // крупные просто "отваливаются"
            } else if (shard.classList.contains('medium')) {
                distance = 80 + Math.random() * 140;
            } else {
                distance = 40 + Math.random() * 100;
            }

            shard.style.left = centerX + 'px';
            shard.style.top = centerY + 'px';

            shard.style.setProperty('--dx', Math.cos(angle) * distance + 'px');
            shard.style.setProperty('--dy', Math.sin(angle) * distance + 'px');

            document.body.appendChild(shard);

            setTimeout(() => shard.remove(), 1400);
        }

        // =========================
        // ЛЁГКИЙ ДЫМ (спокойное исчезновение)
        // =========================
        for (let i = 0; i < 10; i++) {

            const smoke = document.createElement('div');
            smoke.className = 'vk-smoke';

            smoke.style.left = centerX + (Math.random() - 0.5) * 100 + 'px';
            smoke.style.top = centerY + (Math.random() - 0.5) * 80 + 'px';

            document.body.appendChild(smoke);
            setTimeout(() => smoke.remove(), 1500);
        }

    } catch (e) {
        console.warn('Popup close FX error:', e);
    }

    // =========================
    // CLOSE ANIMATION (плавное "оседание")
    // =========================
    popup.css({
        opacity: '0',
        transform: 'scale(0.92) translateY(15px)',
        filter: 'blur(3px)'
    });

    overlay.css('opacity', '0');

    setTimeout(() => {
        popup.remove();
        overlay.remove();
    }, 500);
};

popup.find('.vk-ui-close').on('click', closePopup);

overlay.on('click', e => {
     if(e.target === overlay[0]){
         closePopup();
     }
});
}

/* =========================
МОДАЛКА УДАЛЕНИЯ
========================= */
function ensureModal(){
if ($('.vk-modal-overlay').length) return;
$('body').append(`
<div class="vk-modal-overlay">
<div class="vk-modal">
<div class="vk-modal-user">
<div class="vk-modal-avatar"><img src="" alt=""></div>
<div>
<div class="vk-modal-user-name"></div>
<div class="vk-modal-user-sub">будет удалён из друзей</div>
</div>
</div>
<div class="vk-modal-title">Удаление друга</div>
<div class="vk-modal-text"></div>
<div class="vk-modal-warning">Это действие нельзя отменить</div>
<div class="vk-modal-actions">
<button class="vk-modal-cancel">Отмена</button>
<button class="vk-modal-confirm">Удалить</button>
</div>
</div>
</div>
`);
}

function openDeleteModal(name, ava, onConfirm){
ensureModal();
const modal = $('.vk-modal-overlay');
modal.find('.vk-modal-text').text(`Удалить "${name}" из друзей?`);
modal.find('.vk-modal-avatar img').attr('src', ava || DEFAULT_AVA);
modal.find('.vk-modal-user-name').text(name);

modal.addClass('active');
$('body').addClass('vk-modal-open');

function closeModal(){
modal.removeClass('active');
$('body').removeClass('vk-modal-open');
}

let locked = false;

modal.find('.vk-modal-cancel').off('click').on('click', () => { if(!locked) closeModal(); });

modal.find('.vk-modal-confirm').off('click').on('click', function(){
if(locked) return;
locked = true;
const btn = $(this);
btn.text('Удаление...').css({'pointer-events':'none', 'opacity':'0.7'});

setTimeout(() => {
closeModal();
if(typeof onConfirm === 'function') onConfirm();
btn.text('Удалить').css({'pointer-events':'auto', 'opacity':'1'});
locked = false;
}, 180);
});

modal.off('click').on('click', function(e){
    if(e.target === this && !locked) closeModal();
});
$(document).off('keydown.vkmodal').on('keydown.vkmodal', e => { if(e.key === 'Escape' && !locked) closeModal(); });
}

/* =========================
ЗАГРУЗКА ДАННЫХ — ПОЛНАЯ ВЕРСИЯ (с пагинацией)
========================= */
let currentPage = 1;
let maxPage = null;
let isLoading = false;

function loadFriends({ append = false, silent = false } = {}) {
if (isLoading) return;
isLoading = true;

if (!silent) $("body").css("cursor", "wait");

$.get(`/blog/0-0-${currentPage}-0-17-${USER_ID}?${Math.random()}`, html => {
const $tmp = $('<div>').html(html);
let loaded = [];

// определяем количество страниц
if ($tmp.find("#pagesBlock1").length && currentPage === 1) {
const lastPage = $tmp.find("a.swchItem").eq(-2).text();
maxPage = parseInt(lastPage) || null;
}

$tmp.find('.friend').each(function(){
const $el = $(this);

let group = $el.find('.gr').text().trim();

if (!group) {
const text = $el.text();
const match = text.match(/(Кумир|Друг|Семья|Приятель|Знакомый|Коллега)/i);
group = match ? match[0] : 'Без статуса';
}

loaded.push({
nick: $el.find('.nick').text().trim(),
ava: $el.find('.ava').text().trim() || DEFAULT_AVA,
profile: $el.find('.url').text().trim(),
group: group,
del: $el.find('.del').text().trim() || null
});
});

if (append) {
const existing = new Set(allFriends.map(f => f.nick));
loaded = loaded.filter(f => !existing.has(f.nick));
allFriends = allFriends.concat(loaded);
} else {
allFriends = loaded;
}

localStorage.setItem(LS_FRIENDS, JSON.stringify(allFriends));

if (!silent) {
renderMini(allFriends);
}

}).fail(() => {
console.warn('Не удалось загрузить список друзей');
}).always(() => {
isLoading = false;
$("body").css("cursor", "default");
});
}

/* =========================
ДОГРУЗКА (как в VK)
========================= */
function loadMoreFriends() {
if (isLoading) return;
if (maxPage && currentPage >= maxPage) return;

currentPage++;
loadFriends({ append: true });
}

/* =========================
ИНИЦИАЛИЗАЦИЯ
========================= */

// открытие popup
$counter.add($footer)
.css('cursor','pointer')
.attr('title','Показать всех друзей')
.on('click', showPopup);

// загрузка из кеша
try {
const cached = JSON.parse(localStorage.getItem(LS_FRIENDS) || "[]");
allFriends = cached;
renderMini(allFriends);
} catch(e){
renderMini([]);
}

// первая загрузка
setTimeout(() => {
currentPage = 1;
loadFriends();
}, 800);

// автообновление (тихо)
setInterval(() => {
currentPage = 1;
loadFriends({ silent: true });
}, 30000);

})();
</script>


Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Четверг, 02.04.2026, 17:19 | Сообщение # 2 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:232
         Регистрация:20.10.2016

Исходной код стал настолько массивным, что я уже не мог даже создать тему пришлось использовать обходные пути. Пришло время его «упаковать», хотя потенциал ещё огромный: можно добавить личные сообщения с функцией отправки, сделать более мощный модуль друзей и внедрить ещё множество возможностей.

При этом система остаётся очень лёгкой загружается быстро, без использования тяжёлых графических элементов. По сути, я создаю сайт примерно на 90% на JavaScript. 2

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Пятница, 03.04.2026, 06:18 | Сообщение # 3 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:232
         Регистрация:20.10.2016

Я действительно разрабатывал эффекты взрыва и другие визуальные эффекты.
Если кто-то не поверил , ничего страшного, просто не люблю людей, которые постоянно врут 2
Видео размером 100 МБ сжал до 10 МБ.

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: