ghassan 0b2e95ea65 Add NAS file manager integration and all pending platform changes
- 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>
2026-05-13 13:24:32 +03:00

417 lines
15 KiB
PHP

@props([
'name' => '',
'id' => null,
'value' => null,
'label' => null,
'required' => false,
'class' => '',
'style' => '',
'minYear' => 1900,
'maxYear' => null,
])
@php
$maxYear = (int) ($maxYear ?? date('Y'));
$minYear = (int) $minYear;
$uid = 'dp_' . ($id ?? $name) . '_' . substr(md5(uniqid()), 0, 8);
$inputId = $id ?? $name;
// Parse YYYY-MM-DD initial value
$initDay = $initMonth = $initYear = null;
if ($value && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m)) {
[, $initYear, $initMonth, $initDay] = array_map('intval', $m);
}
$months = ['January','February','March','April','May','June',
'July','August','September','October','November','December'];
@endphp
{{-- ── Shared CSS ────────────────────────────────────────────────────── --}}
@once('dp-styles')
<style>
.dp-wrap { position: relative; }
.dp-lbl { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: var(--text-primary, #f1f1f1); }
.dp-lbl .req { color: var(--brand-red, #e61e1e); margin-left: 2px; }
.dp-row { display: flex; gap: 8px; }
/* Each column */
.dp-field { position: relative; flex: 1; min-width: 0; }
.dp-field.dp-day { flex: 0 0 72px; }
.dp-field.dp-year { flex: 0 0 98px; }
/* Trigger button — matches .form-input style */
.dp-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
width: 100%;
background: var(--bg-dark, #0f0f0f);
border: 1px solid var(--border-color, #303030);
border-radius: 8px;
padding: 10px 10px;
color: var(--text-primary, #f1f1f1);
font-size: 14px;
font-family: inherit;
line-height: 1.4;
cursor: pointer;
text-align: left;
transition: border-color .2s;
outline: none;
min-height: 46px;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
}
.dp-btn:hover,
.dp-btn:focus-visible { border-color: var(--brand-red, #e61e1e); }
.dp-btn[aria-expanded="true"] { border-color: var(--brand-red, #e61e1e); }
.dp-val { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.dp-val.ph { color: var(--text-secondary, #aaa); }
.dp-arr { flex-shrink: 0; font-size: 10px; color: var(--text-secondary, #aaa); transition: transform .2s; }
.dp-btn[aria-expanded="true"] .dp-arr { transform: rotate(180deg); }
/* Dropdown panel */
.dp-panel {
position: fixed;
z-index: 1060;
background: var(--bg-secondary, #1e1e1e);
border: 1px solid var(--border-color, #303030);
border-radius: 10px;
box-shadow: 0 12px 32px rgba(0,0,0,.55);
overflow: hidden;
}
/* Year search bar */
.dp-srch {
display: flex;
align-items: center;
gap: 7px;
padding: 7px 10px;
border-bottom: 1px solid var(--border-color, #303030);
}
.dp-srch i { font-size: 12px; color: var(--text-secondary, #aaa); flex-shrink: 0; }
.dp-sinput {
flex: 1; width: 100%;
background: none; border: none; outline: none;
color: var(--text-primary, #f1f1f1);
font-size: 13px; font-family: inherit;
}
.dp-sinput::placeholder { color: var(--text-secondary, #aaa); }
/* Shared list */
.dp-list {
list-style: none;
margin: 0; padding: 4px 0;
max-height: 220px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-color, #303030) transparent;
}
.dp-list::-webkit-scrollbar { width: 4px; }
.dp-list::-webkit-scrollbar-thumb { background: var(--border-color, #303030); border-radius: 2px; }
/* Month / Year options (list style) */
.dp-list-opt {
display: flex;
align-items: center;
padding: 8px 13px;
cursor: pointer;
font-size: 13px;
color: var(--text-primary, #f1f1f1);
transition: background .1s;
outline: none;
}
.dp-list-opt:hover { background: rgba(255,255,255,.07); }
.dp-list-opt[aria-selected="true"] {
background: rgba(230,30,30,.12);
color: var(--brand-red, #e61e1e);
font-weight: 600;
}
/* Day grid */
.dp-day-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 3px;
padding: 8px;
max-height: none;
overflow: visible;
}
.dp-day-opt {
display: flex;
align-items: center;
justify-content: center;
padding: 7px 2px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--text-primary, #f1f1f1);
transition: background .1s;
outline: none;
user-select: none;
}
.dp-day-opt:hover { background: rgba(255,255,255,.1); }
.dp-day-opt[aria-selected="true"] {
background: var(--brand-red, #e61e1e);
color: #fff;
font-weight: 700;
}
.dp-day-opt[hidden] { display: none; }
.dp-list-opt[hidden] { display: none; }
</style>
@endonce
{{-- ── Shared JS ─────────────────────────────────────────────────────── --}}
@once('dp-script')
<script>
(function () {
if (window.DatePicker) return;
window.DatePicker = class {
constructor(id) {
this.root = document.getElementById(id);
this.hidden = this.root.querySelector('input[type=hidden]');
this.f = {
day: this.root.querySelector('.dp-day'),
month: this.root.querySelector('.dp-month'),
year: this.root.querySelector('.dp-year'),
};
this.v = { day: null, month: null, year: null };
// Restore from hidden value
const raw = this.hidden.value;
if (raw && /^\d{4}-\d{2}-\d{2}$/.test(raw)) {
const [y, m, d] = raw.split('-').map(Number);
this.v = { year: y, month: m, day: d };
}
this._bind('day');
this._bind('month');
this._bind('year');
// Apply initial day constraints
if (this.v.month) this._constrainDays();
document.addEventListener('click', e => {
if (!this.root.contains(e.target)) this._closeAll();
}, true);
}
_bind(type) {
const field = this.f[type];
const btn = field.querySelector('.dp-btn');
const panel = field.querySelector('.dp-panel');
const list = field.querySelector('[data-dp-list]');
const sinput = field.querySelector('.dp-sinput');
btn.addEventListener('click', e => {
e.stopPropagation();
const wasOpen = !panel.hidden;
this._closeAll();
if (wasOpen) return;
panel.hidden = false;
btn.setAttribute('aria-expanded', 'true');
if (sinput) {
sinput.value = '';
this._filterYear(list, '');
requestAnimationFrame(() => sinput.focus());
}
// Scroll selected option into view
requestAnimationFrame(() => {
const sel = list.querySelector('[aria-selected="true"]');
if (sel) sel.scrollIntoView({ block: 'nearest' });
});
// Position panel using fixed coords to escape any overflow container
const r = btn.getBoundingClientRect();
const minW = type === 'day' ? 196 : r.width;
const goUp = window.innerHeight - r.bottom < 270 && r.top > 270;
panel.style.left = r.left + 'px';
panel.style.width = Math.max(r.width, minW) + '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 = '';
}
});
list.addEventListener('click', e => {
const opt = e.target.closest('[data-v]');
if (opt && !opt.hidden) this._pick(type, parseInt(opt.dataset.v));
});
if (sinput) {
sinput.addEventListener('input', () => this._filterYear(list, sinput.value));
sinput.addEventListener('keydown', e => {
if (e.key === 'Escape') this._closeAll();
});
}
}
_pick(type, val) {
const field = this.f[type];
const valEl = field.querySelector('.dp-val');
const list = field.querySelector('[data-dp-list]');
const MONTHS = ['January','February','March','April','May','June',
'July','August','September','October','November','December'];
this.v[type] = val;
valEl.textContent = type === 'month' ? MONTHS[val - 1] : String(val);
valEl.classList.remove('ph');
list.querySelectorAll('[data-v]').forEach(o =>
o.setAttribute('aria-selected', parseInt(o.dataset.v) === val ? 'true' : 'false')
);
this._closeAll();
if (type === 'month' || type === 'year') this._constrainDays();
this._flush();
}
_constrainDays() {
const m = this.v.month || 1;
const y = this.v.year || 2000;
const max = new Date(y, m, 0).getDate(); // last day of month
const list = this.f.day.querySelector('[data-dp-list]');
list.querySelectorAll('[data-v]').forEach(o => {
const d = parseInt(o.dataset.v);
o.hidden = d > max;
if (d > max && this.v.day === d) {
this.v.day = null;
const valEl = this.f.day.querySelector('.dp-val');
valEl.textContent = 'Day';
valEl.classList.add('ph');
o.setAttribute('aria-selected', 'false');
}
});
}
_filterYear(list, q) {
list.querySelectorAll('[data-v]').forEach(o => {
o.style.display = (q.length > 0 && !o.dataset.v.startsWith(q)) ? 'none' : '';
});
}
_closeAll() {
this.root.querySelectorAll('.dp-panel').forEach(p => { p.hidden = true; });
this.root.querySelectorAll('.dp-btn').forEach(b => b.setAttribute('aria-expanded', 'false'));
}
_flush() {
const { day, month, year } = this.v;
if (day && month && year) {
this.hidden.value =
`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
} else {
this.hidden.value = '';
}
this.hidden.dispatchEvent(new Event('change', { bubbles: true }));
}
};
}());
</script>
@endonce
{{-- ── Component HTML ─────────────────────────────────────────────────── --}}
<div class="dp-wrap {{ $class }}" id="{{ $uid }}" @if($style) style="{{ $style }}" @endif>
@if($label)
<label class="dp-lbl" for="{{ $inputId }}">
{{ $label }}@if($required)<span class="req">*</span>@endif
</label>
@endif
<div class="dp-row">
{{-- ── Day ──────────────────────────────────────────────────────── --}}
<div class="dp-field dp-day">
<button type="button" class="dp-btn" aria-expanded="false" aria-label="Day">
<span class="dp-val{{ !$initDay ? ' ph' : '' }}">{{ $initDay ?? 'Day' }}</span>
<i class="bi bi-chevron-down dp-arr"></i>
</button>
<div class="dp-panel" hidden>
<ul class="dp-list dp-day-grid" data-dp-list>
@for($d = 1; $d <= 31; $d++)
<li class="dp-day-opt"
data-v="{{ $d }}"
aria-selected="{{ $initDay === $d ? 'true' : 'false' }}"
role="option">{{ $d }}</li>
@endfor
</ul>
</div>
</div>
{{-- ── Month ─────────────────────────────────────────────────────── --}}
<div class="dp-field dp-month">
<button type="button" class="dp-btn" aria-expanded="false" aria-label="Month">
<span class="dp-val{{ !$initMonth ? ' ph' : '' }}">
{{ $initMonth ? $months[$initMonth - 1] : 'Month' }}
</span>
<i class="bi bi-chevron-down dp-arr"></i>
</button>
<div class="dp-panel" hidden>
<ul class="dp-list" data-dp-list>
@foreach($months as $mi => $mname)
<li class="dp-list-opt"
data-v="{{ $mi + 1 }}"
aria-selected="{{ $initMonth === ($mi + 1) ? 'true' : 'false' }}"
role="option">{{ $mname }}</li>
@endforeach
</ul>
</div>
</div>
{{-- ── Year ──────────────────────────────────────────────────────── --}}
<div class="dp-field dp-year">
<button type="button" class="dp-btn" aria-expanded="false" aria-label="Year">
<span class="dp-val{{ !$initYear ? ' ph' : '' }}">{{ $initYear ?? 'Year' }}</span>
<i class="bi bi-chevron-down dp-arr"></i>
</button>
<div class="dp-panel" hidden>
<div class="dp-srch">
<i class="bi bi-search"></i>
<input type="text" class="dp-sinput" placeholder="Year…" autocomplete="off" spellcheck="false">
</div>
<ul class="dp-list" data-dp-list>
@for($y = $maxYear; $y >= $minYear; $y--)
<li class="dp-list-opt"
data-v="{{ $y }}"
aria-selected="{{ $initYear === $y ? 'true' : 'false' }}"
role="option">{{ $y }}</li>
@endfor
</ul>
</div>
</div>
</div>{{-- .dp-row --}}
<input type="hidden"
name="{{ $name }}"
id="{{ $inputId }}"
value="{{ $value }}"
@if($required) required @endif>
</div>{{-- .dp-wrap --}}
<script>
(function () {
function boot() { new DatePicker('{{ $uid }}'); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
}());
</script>