- Installed p7h/nas-file-manager package via private VCS repo - Published config/nas-file-manager.php with super_admin middleware restriction - Added NAS env vars to .env.example - Created admin/nas-storage page with connection info panel and file browser widget - Added NAS Storage link to admin sidebar (super_admin only) - Added SuperAdminController@nasStorage method and admin.nas-storage route - Includes all accumulated branch changes: profile wall, 2FA, audit logs, settings panel, country/phone/timezone components, posts, slideshow, playlist shares, video downloads/shares, comment likes, notifications, social links, and more Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
325 lines
16 KiB
PHP
325 lines
16 KiB
PHP
@props([
|
|
'existing' => [], // [ ['platform'=>'twitter','value'=>'handle','visibility'=>'public'], ... ]
|
|
])
|
|
|
|
@php
|
|
$uid = 'sle_' . substr(md5(uniqid()), 0, 8);
|
|
@endphp
|
|
|
|
@once('sle-styles')
|
|
<style>
|
|
.sle-entry { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
.sle-entry-input { flex: 1; min-width: 0; }
|
|
|
|
.sle-icon-btn {
|
|
flex-shrink: 0; display: flex; align-items: center; justify-content: center;
|
|
width: 38px; height: 38px; border-radius: 8px; background: transparent;
|
|
border: 1px solid var(--border-color, #303030);
|
|
font-size: 15px; cursor: pointer; transition: border-color .15s, color .15s, background .15s;
|
|
}
|
|
.sle-remove-btn { color: var(--text-secondary, #aaa); }
|
|
.sle-remove-btn:hover { border-color: #e61e1e; color: #e61e1e; background: rgba(230,30,30,.08); }
|
|
|
|
/* Visibility button colour per state */
|
|
.sle-vis-btn[data-vis="public"] { color: #22c55e; border-color: rgba(34,197,94,.35); }
|
|
.sle-vis-btn[data-vis="registered"] { color: #3b82f6; border-color: rgba(59,130,246,.35); }
|
|
.sle-vis-btn[data-vis="subscribers"] { color: #f59e0b; border-color: rgba(245,158,11,.35); }
|
|
.sle-vis-btn[data-vis="only_me"] { color: #6b7280; border-color: rgba(107,114,128,.35); }
|
|
|
|
.sle-empty {
|
|
text-align: center; padding: 28px 16px;
|
|
color: var(--text-secondary, #aaa); font-size: 14px;
|
|
}
|
|
.sle-empty i { font-size: 30px; display: block; margin-bottom: 8px; opacity: .45; }
|
|
|
|
/* Shared popup panel */
|
|
.sle-popup-wrap { position: relative; }
|
|
.sle-popup {
|
|
position: fixed; z-index: 2100;
|
|
background: var(--bg-secondary, #1e1e1e); border: 1px solid var(--border-color, #303030);
|
|
border-radius: 10px; box-shadow: 0 12px 32px rgba(0,0,0,.6); padding: 4px 0;
|
|
}
|
|
|
|
.sle-pop-opt {
|
|
display: flex; align-items: center; gap: 9px; padding: 8px 13px;
|
|
cursor: pointer; font-size: 13px; color: var(--text-primary, #f1f1f1);
|
|
transition: background .1s; white-space: nowrap;
|
|
}
|
|
.sle-pop-opt:hover { background: rgba(255,255,255,.07); }
|
|
.sle-pop-opt.active { font-weight: 600; }
|
|
.sle-pop-opt i { font-size: 14px; flex-shrink: 0; width: 16px; text-align: center; }
|
|
|
|
/* Platform picker panel is scrollable */
|
|
.sle-popup.plat-panel {
|
|
overflow-y: auto; max-height: 260px;
|
|
scrollbar-width: thin; scrollbar-color: var(--border-color, #303030) transparent;
|
|
}
|
|
.sle-popup.plat-panel::-webkit-scrollbar { width: 4px; }
|
|
.sle-popup.plat-panel::-webkit-scrollbar-thumb { background: var(--border-color, #303030); border-radius: 2px; }
|
|
|
|
/* Platform picker button */
|
|
.sle-sp-wrap { position: relative; flex: 0 0 160px; }
|
|
.sle-sp-btn {
|
|
display: flex; align-items: center; gap: 8px; width: 100%;
|
|
background: var(--bg-dark, #0f0f0f); border: 1px solid var(--border-color, #303030);
|
|
border-radius: 8px; padding: 9px 10px; color: var(--text-primary, #f1f1f1);
|
|
font-size: 13px; font-family: inherit; cursor: pointer; text-align: left;
|
|
transition: border-color .2s; outline: none; min-height: 38px; box-sizing: border-box;
|
|
}
|
|
.sle-sp-btn:hover, .sle-sp-btn:focus-visible { border-color: var(--brand-red, #e61e1e); }
|
|
.sle-sp-btn[aria-expanded="true"] { border-color: var(--brand-red, #e61e1e); }
|
|
.sle-sp-ico { font-size: 15px; flex-shrink: 0; }
|
|
.sle-sp-val { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.sle-sp-val.ph { color: var(--text-secondary, #aaa); }
|
|
.sle-sp-arr { font-size: 10px; color: var(--text-secondary, #aaa); flex-shrink: 0; transition: transform .2s; }
|
|
.sle-sp-btn[aria-expanded="true"] .sle-sp-arr { transform: rotate(180deg); }
|
|
</style>
|
|
@endonce
|
|
|
|
@once('sle-script')
|
|
<script>
|
|
(function () {
|
|
if (window.SocialLinksEditor) return;
|
|
|
|
// Close all popups when clicking outside
|
|
document.addEventListener('click', e => {
|
|
if (!e.target.closest('.sle-popup-wrap') && !e.target.closest('.sle-sp-wrap')) {
|
|
document.querySelectorAll('.sle-popup').forEach(p => { p.hidden = true; });
|
|
document.querySelectorAll('[aria-expanded="true"].sle-sp-btn').forEach(b => b.setAttribute('aria-expanded','false'));
|
|
}
|
|
});
|
|
|
|
window.SocialLinksEditor = class {
|
|
constructor(uid, existing) {
|
|
this.root = document.getElementById(uid);
|
|
this.entriesEl = this.root.querySelector('[data-sle-entries]');
|
|
this.addBtn = this.root.querySelector('[data-sle-add]');
|
|
this._idx = 0;
|
|
|
|
this.PLATFORMS = [
|
|
{ key: 'twitter', label: 'Twitter / X', icon: 'bi-twitter-x', placeholder: 'username', type: 'text', defaultVis: 'public' },
|
|
{ key: 'instagram', label: 'Instagram', icon: 'bi-instagram', placeholder: 'username', type: 'text', defaultVis: 'public' },
|
|
{ key: 'facebook', label: 'Facebook', icon: 'bi-facebook', placeholder: 'username', type: 'text', defaultVis: 'public' },
|
|
{ key: 'youtube', label: 'YouTube', icon: 'bi-youtube', placeholder: 'channel', type: 'text', defaultVis: 'public' },
|
|
{ key: 'linkedin', label: 'LinkedIn', icon: 'bi-linkedin', placeholder: 'username', type: 'text', defaultVis: 'public' },
|
|
{ key: 'tiktok', label: 'TikTok', icon: 'bi-tiktok', placeholder: 'username', type: 'text', defaultVis: 'public' },
|
|
{ key: 'whatsapp', label: 'WhatsApp', icon: 'bi-whatsapp', placeholder: '97312345678 (no + or spaces)', type: 'text', defaultVis: 'subscribers' },
|
|
{ key: 'website', label: 'Website', icon: 'bi-globe', placeholder: 'https://example.com', type: 'url', defaultVis: 'public' },
|
|
{ key: 'google_location', label: 'Google Maps', icon: 'bi-geo-alt-fill', placeholder: 'https://maps.google.com/...', type: 'url', defaultVis: 'public' },
|
|
{ key: 'social_phone', label: 'Phone', icon: 'bi-telephone-fill', placeholder: '+97312345678', type: 'text', defaultVis: 'subscribers' },
|
|
{ key: 'social_email', label: 'Email', icon: 'bi-envelope-fill', placeholder: 'email@example.com', type: 'email', defaultVis: 'subscribers' },
|
|
];
|
|
|
|
this.VISIBILITIES = [
|
|
{ key: 'public', label: 'Public', icon: 'bi-globe2', color: '#22c55e', hint: 'Everyone including guests' },
|
|
{ key: 'registered', label: 'Registered', icon: 'bi-person-check-fill', color: '#3b82f6', hint: 'Logged-in users only' },
|
|
{ key: 'subscribers', label: 'Subscribers', icon: 'bi-star-fill', color: '#f59e0b', hint: 'Your subscribers only' },
|
|
{ key: 'only_me', label: 'Only me', icon: 'bi-lock-fill', color: '#6b7280', hint: 'Hidden from everyone else' },
|
|
];
|
|
|
|
existing.forEach(({ platform, value, visibility }) => {
|
|
if (platform && value) {
|
|
this.entriesEl.appendChild(this._buildEntry(platform, value, visibility || 'public'));
|
|
}
|
|
});
|
|
this._renderEmpty();
|
|
|
|
this.addBtn.addEventListener('click', () => {
|
|
const row = this._buildEntry('', '', 'public');
|
|
this.entriesEl.appendChild(row);
|
|
this._renderEmpty();
|
|
row.querySelector('.sle-sp-btn').click();
|
|
});
|
|
}
|
|
|
|
_platform(key) { return this.PLATFORMS.find(p => p.key === key) || null; }
|
|
_visibility(key) { return this.VISIBILITIES.find(v => v.key === key) || this.VISIBILITIES[0]; }
|
|
|
|
_renderEmpty() {
|
|
let empty = this.entriesEl.querySelector('.sle-empty');
|
|
if (this.entriesEl.querySelectorAll('.sle-entry').length === 0) {
|
|
if (!empty) {
|
|
empty = document.createElement('div');
|
|
empty.className = 'sle-empty';
|
|
empty.innerHTML = '<i class="bi bi-share"></i>No social accounts added yet.';
|
|
this.entriesEl.appendChild(empty);
|
|
}
|
|
} else {
|
|
if (empty) empty.remove();
|
|
}
|
|
}
|
|
|
|
_closeAllPopups() {
|
|
document.querySelectorAll('.sle-popup').forEach(p => { p.hidden = true; });
|
|
document.querySelectorAll('[aria-expanded="true"].sle-sp-btn').forEach(b => b.setAttribute('aria-expanded','false'));
|
|
}
|
|
|
|
_togglePopup(trigger, panel) {
|
|
const wasOpen = !panel.hidden;
|
|
this._closeAllPopups();
|
|
if (!wasOpen) {
|
|
panel.hidden = false;
|
|
if (trigger.classList.contains('sle-sp-btn')) trigger.setAttribute('aria-expanded','true');
|
|
const r = trigger.getBoundingClientRect();
|
|
const isRight = panel.classList.contains('pop-right');
|
|
const goUp = window.innerHeight - r.bottom < 270 && r.top > 270;
|
|
panel.style.minWidth = r.width + 'px';
|
|
if (goUp) {
|
|
panel.style.top = '';
|
|
panel.style.bottom = (window.innerHeight - r.top + 4) + 'px';
|
|
} else {
|
|
panel.style.top = (r.bottom + 4) + 'px';
|
|
panel.style.bottom = '';
|
|
}
|
|
if (isRight) {
|
|
panel.style.left = '';
|
|
panel.style.right = (window.innerWidth - r.right) + 'px';
|
|
} else {
|
|
panel.style.left = r.left + 'px';
|
|
panel.style.right = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
_buildEntry(key, value, visibility) {
|
|
const idx = this._idx++;
|
|
const p = this._platform(key);
|
|
const vis = this._visibility(visibility || (p ? p.defaultVis : 'public'));
|
|
|
|
const row = document.createElement('div');
|
|
row.className = 'sle-entry';
|
|
|
|
// ── 1. Platform picker ───────────────────────────────────────
|
|
const spWrap = document.createElement('div');
|
|
spWrap.className = 'sle-sp-wrap';
|
|
|
|
const spBtn = document.createElement('button');
|
|
spBtn.type = 'button'; spBtn.className = 'sle-sp-btn'; spBtn.setAttribute('aria-expanded','false');
|
|
|
|
const spIco = document.createElement('i');
|
|
spIco.className = p ? `sle-sp-ico bi ${p.icon}` : 'sle-sp-ico bi bi-plus-circle';
|
|
const spVal = document.createElement('span');
|
|
spVal.className = p ? 'sle-sp-val' : 'sle-sp-val ph';
|
|
spVal.textContent = p ? p.label : 'Platform';
|
|
const spArr = document.createElement('i');
|
|
spArr.className = 'bi bi-chevron-down sle-sp-arr';
|
|
spBtn.append(spIco, spVal, spArr);
|
|
|
|
const platInput = document.createElement('input');
|
|
platInput.type = 'hidden'; platInput.name = `slink[${idx}][platform]`; platInput.value = key || '';
|
|
|
|
const spPanel = document.createElement('div');
|
|
spPanel.className = 'sle-popup plat-panel'; spPanel.hidden = true;
|
|
|
|
this.PLATFORMS.forEach(pl => {
|
|
const opt = document.createElement('div');
|
|
opt.className = `sle-pop-opt${pl.key === key ? ' active' : ''}`;
|
|
opt.innerHTML = `<i class="bi ${pl.icon}"></i>${pl.label}`;
|
|
opt.addEventListener('click', () => {
|
|
spIco.className = `sle-sp-ico bi ${pl.icon}`;
|
|
spVal.textContent = pl.label; spVal.classList.remove('ph');
|
|
spPanel.querySelectorAll('.sle-pop-opt').forEach(o => o.classList.remove('active'));
|
|
opt.classList.add('active');
|
|
platInput.value = pl.key;
|
|
inp.type = pl.type;
|
|
inp.placeholder = pl.placeholder;
|
|
inp.disabled = false;
|
|
inp.value = '';
|
|
// Apply default visibility for this platform
|
|
const defVis = this._visibility(pl.defaultVis);
|
|
visInput.value = defVis.key;
|
|
visBtn.dataset.vis = defVis.key;
|
|
visBtn.innerHTML = `<i class="bi ${defVis.icon}"></i>`;
|
|
visBtn.title = defVis.label + ' — ' + defVis.hint;
|
|
this._closeAllPopups();
|
|
inp.focus();
|
|
});
|
|
spPanel.appendChild(opt);
|
|
});
|
|
|
|
spBtn.addEventListener('click', e => { e.stopPropagation(); this._togglePopup(spBtn, spPanel); });
|
|
spWrap.append(spBtn, platInput, spPanel);
|
|
|
|
// ── 2. Value input ───────────────────────────────────────────
|
|
const inp = document.createElement('input');
|
|
inp.className = 'form-input sle-entry-input';
|
|
inp.type = p ? p.type : 'text';
|
|
inp.name = `slink[${idx}][value]`;
|
|
inp.placeholder = p ? p.placeholder : 'select platform first';
|
|
inp.disabled = !key;
|
|
inp.value = value || '';
|
|
|
|
// ── 3. Visibility picker ─────────────────────────────────────
|
|
const visWrap = document.createElement('div');
|
|
visWrap.className = 'sle-popup-wrap';
|
|
|
|
const visInput = document.createElement('input');
|
|
visInput.type = 'hidden'; visInput.name = `slink[${idx}][visibility]`; visInput.value = vis.key;
|
|
|
|
const visBtn = document.createElement('button');
|
|
visBtn.type = 'button';
|
|
visBtn.className = 'sle-icon-btn sle-vis-btn';
|
|
visBtn.dataset.vis = vis.key;
|
|
visBtn.title = vis.label + ' — ' + vis.hint;
|
|
visBtn.innerHTML = `<i class="bi ${vis.icon}"></i>`;
|
|
|
|
const visPanel = document.createElement('div');
|
|
visPanel.className = 'sle-popup pop-right'; visPanel.hidden = true;
|
|
|
|
this.VISIBILITIES.forEach(v => {
|
|
const opt = document.createElement('div');
|
|
opt.className = `sle-pop-opt${v.key === vis.key ? ' active' : ''}`;
|
|
opt.style.color = v.key === vis.key ? v.color : '';
|
|
opt.innerHTML = `<i class="bi ${v.icon}" style="color:${v.color}"></i>`
|
|
+ `<span><strong>${v.label}</strong><br><small style="color:var(--text-secondary,#aaa)">${v.hint}</small></span>`;
|
|
opt.addEventListener('click', () => {
|
|
visInput.value = v.key;
|
|
visBtn.dataset.vis = v.key;
|
|
visBtn.innerHTML = `<i class="bi ${v.icon}"></i>`;
|
|
visBtn.title = v.label + ' — ' + v.hint;
|
|
visPanel.querySelectorAll('.sle-pop-opt').forEach(o => {
|
|
o.classList.remove('active'); o.style.color = '';
|
|
});
|
|
opt.classList.add('active'); opt.style.color = v.color;
|
|
visPanel.hidden = true;
|
|
});
|
|
visPanel.appendChild(opt);
|
|
});
|
|
|
|
visBtn.addEventListener('click', e => { e.stopPropagation(); this._togglePopup(visBtn, visPanel); });
|
|
visWrap.append(visInput, visBtn, visPanel);
|
|
|
|
// ── 4. Remove button ─────────────────────────────────────────
|
|
const rm = document.createElement('button');
|
|
rm.type = 'button'; rm.className = 'sle-icon-btn sle-remove-btn'; rm.title = 'Remove';
|
|
rm.innerHTML = '<i class="bi bi-trash"></i>';
|
|
rm.addEventListener('click', () => { row.remove(); this._renderEmpty(); });
|
|
|
|
row.append(spWrap, inp, visWrap, rm);
|
|
return row;
|
|
}
|
|
};
|
|
}());
|
|
</script>
|
|
@endonce
|
|
|
|
<div id="{{ $uid }}">
|
|
<div data-sle-entries></div>
|
|
<div style="margin-top:4px;">
|
|
<button type="button" data-sle-add class="action-btn">
|
|
<i class="bi bi-plus-lg"></i> <span>Add Account</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
function boot() { new SocialLinksEditor('{{ $uid }}', @json($existing)); }
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
}());
|
|
</script>
|