- 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>
417 lines
15 KiB
PHP
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>
|