A lot of things

- Added Database page.
- Added Xenforo API compatibility
- Added Hovercard
- Added Notifications
This commit is contained in:
2026-05-24 11:47:20 +02:00
parent 7cd6dfddda
commit a778222564
51 changed files with 3228 additions and 38 deletions

View File

@@ -0,0 +1,32 @@
<div class="filter-group" x-data="{open:false,search:''}">
<div class="filter-title-row" @click="open = !open">
<div class="filter-title-left">
<h4 class="filter-title">{{ $title }}</h4>
<span class="internal-filter-count" x-show="$wire.{{ $model }}.length > 0" x-text="$wire.{{ $model }}.length"></span>
</div>
<div class="filter-title-right" @click.stop>
<div class="filter-mode">
<button type="button" wire:click="$set('{{ $modeModel }}', 'or')" class="filter-btn-mode {{ $selectedMode === 'or' ? 'active' : '' }}">OR</button>
<button type="button" wire:click="$set('{{ $modeModel }}', 'and')" class="filter-btn-mode {{ $selectedMode === 'and' ? 'active' : '' }}">AND</button>
</div>
<button type="button" class="internal-filter-clear" x-show="$wire.{{ $model }}.length > 0" @click="$wire.set('{{ $model }}',[])" title="Clear">
<i data-lucide="x" size="11"></i>
</button>
<i data-lucide="chevron-down" size="14" class="filter-chevron" :class="{ 'rotated': !open }"></i>
</div>
</div>
<div x-show="open" x-transition>
<div class="internal-filter-search">
<i data-lucide="search" size="13"></i>
<input type="text" x-model="search" placeholder="Search...">
</div>
<div class="filter-options">
@foreach($items as $item)
<label class="filter-option" x-show="search.length >= 3 && '{{strtolower($item->{$nameProperty}) }}'.includes(search.toLowerCase())">
<input type="checkbox" wire:model.live="{{ $model }}" value="{{ $item->{$idProperty} }}">
{{ $item->{$nameProperty} }}
</label>
@endforeach
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
<div class="filter-group" x-data="{open:false}">
<div class="filter-title-row" @click="open = !open">
<div class="filter-title-left">
<h4 class="filter-title">{{ $title }}</h4>
<span class="internal-filter-count" x-show="$wire.{{ $model }}.length > 0" x-text="$wire.{{ $model }}.length"></span>
</div>
<div class="filter-title-right" @click.stop>
<div class="filter-mode">
<button type="button" wire:click="$set('{{ $modeModel }}', 'or')" class="filter-btn-mode {{ $selectedMode === 'or' ? 'active' : '' }}">OR</button>
<button type="button" wire:click="$set('{{ $modeModel }}', 'and')" class="filter-btn-mode {{ $selectedMode === 'and' ? 'active' : '' }}">AND</button>
</div>
<button type="button" class="internal-filter-clear" x-show="$wire.{{ $model }}.length > 0" @click="$wire.set('{{ $model }}',[])" title="Clear">
<i data-lucide="x" size="11"></i>
</button>
<i data-lucide="chevron-down" size="14" class="filter-chevron" :class="{ 'rotated': !open }"></i>
</div>
</div>
<div class="filter-options" x-show="open" x-transition>
@foreach($items as $item)
<label class="filter-option">
<input type="checkbox" wire:model.live="{{ $model }}" value="{{ $item->{$idProperty} }}">
{{ $item->{$nameProperty} }}
</label>
@endforeach
</div>
</div>

View File

@@ -0,0 +1,28 @@
<div class="filter-group" x-data="{open: false,search:''}">
<div class="filter-title-row" @click="open = !open">
<div class="filter-title-left">
<h4 class="filter-title">{{ $title }}</h4>
<span class="internal-filter-count" x-show="$wire.{{ $model }}.length > 0" x-text="$wire.{{ $model }}.length"></span>
</div>
<div class="filter-title-right" @click.stop>
<button type="button" class="internal-filter-clear" x-show="$wire.{{ $model }}.length > 0" @click="$wire.set('{{ $model }}',[])" title="Clear">
<i data-lucide="x" size="11"></i>
</button>
<i data-lucide="chevron-down" size="14" class="filter-chevron" :class="{ 'rotated': !open }"></i>
</div>
</div>
<div x-show="open" x-transition>
<div class="internal-filter-search">
<i data-lucide="search" size="13"></i>
<input type="text" x-model="search" placeholder="Search...">
</div>
<div class="filter-options">
@foreach($items as $item)
<label class="filter-option" x-show="search.length >= 3 && '{{strtolower($item->{$nameProperty}) }}'.includes(search.toLowerCase())">
<input type="checkbox" wire:model.live="{{ $model }}" value="{{ $item->{$idProperty} }}">
{{ $item->{$nameProperty} }}
</label>
@endforeach
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="filter-group" x-data="{open: false}">
<div class="filter-title-row" @click="open = !open">
<div class="filter-title-left">
<h4 class="filter-title">{{ $title }}</h4>
<span class="internal-filter-count" x-show="$wire.{{ $model }}.length > 0" x-text="$wire.{{ $model }}.length"></span>
</div>
<div class="filter-title-right" @click.stop>
<button type="button" class="internal-filter-clear" x-show="$wire.{{ $model }}.length > 0" @click="$wire.set('{{ $model }}',[])" title="Clear">
<i data-lucide="x" size="11"></i>
</button>
<i data-lucide="chevron-down" size="14" class="filter-chevron" :class="{ 'rotated': !open }"></i>
</div>
</div>
<div class="filter-options" x-show="open" x-transition>
@foreach($items as $item)
<label class="filter-option">
<input type="checkbox" wire:model.live="{{ $model }}" value="{{ $item->{$idProperty} }}">
{{ $item->{$nameProperty} }}
</label>
@endforeach
</div>
</div>

View File

@@ -0,0 +1,40 @@
<div class="entry-card">
<div class="entry-cover-wrapper">
<span class="entry-badge">{{ $entry->getRealPlatform()?->name ?? 'Unknown' }}</span>
@if( $entry->main_image )
<img src="{{ Storage::url($entry->main_image) }}">
@else
<i data-lucide="image" size="40" color="var(--border)"></i>
@endif
</div>
<div class="entry-card-info">
<div class="entry-card-title">{{ $entry->title }}</div>
<div class="entry-card-author">
@forelse( $entry->authors as $author)
@if($loop->first)By @endif
{{ $author-> name }}
@if( !$loop->last ), @endif
@empty
No authors
@endforelse
</div>
<div style="margin-bottom:10px">
<span class="badge {{ $entry->type }}">{{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }}</span>
@if( section_must_be('romhacks', $entry->type ) )
@foreach( $entry->modifications as $modif )
<span class="badge orange">{{ $modif->name }}</span>
@endforeach
@if( $entry->status_id )
<span class="badge">{{ $entry->status->name }}</span>
@endif
@foreach( $entry->languages as $lang )
<span class="badge">{{ $lang->name }}</span>
@endforeach
@endif
</div>
<div class="entry-card-meta">
<span><i data-lucide="download" size="12"></i> x</span>
<span>Added: {{ $entry->created_at->format('y-m-d') }}</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,76 @@
<div
x-data
x-show="$store.hovercard.start"
x-cloak
class="hovercard-overlay hovercard"
:style="{ left: $store.hovercard.x + 'px', top: $store.hovercard.y + 'px' }"
@mouseleave="$store.hovercard.close()"
@keydown.escape.window="$store.hovercard.close()"
>
<template x-if="$store.hovercard.loading">
<div class="hovercard-overlay-loading">
<i data-lucide="loader-2" class="spin"></i>
</div>
</template>
<template x-if="$store.hovercard.error">
<div class="hovercard-overlay-error">
<i data-lucide="alert-circle"></i>
<p>Failed to load profile.</p>
</div>
</template>
<template x-if="$store.hovercard.data && !$store.hovercard.loading && !$store.hovercard.error">
<div>
<div class="hovercard-header">
<div
class="hovercard-avatar"
:style="$store.hovercard.data.avatar_url === null
? { background: $store.hovercard.data.avatar_color }
: {}"
>
<template x-if="$store.hovercard.data.avatar_url !== null">
<img :src="$store.hovercard.data.avatar_url" alt="avatar">
</template>
<template x-if="$store.hovercard.data.avatar_url === null">
<span x-text="$store.hovercard.data.avatar_letter"></span>
</template>
</div>
</div>
<div class="hovercard-body">
<div class="hovercard-username" x-text="$store.hovercard.data.username"></div>
<div class="hovercard-title" x-text="$store.hovercard.data.group_name"></div>
<div class="hovercard-stats">
<div class="hovercard-stat">
<span class="stat-value" x-text="$store.hovercard.data.message_count"></span>
<span class="stat-label">Messages</span>
</div>
<div class="hovercard-stat">
<span class="stat-value" x-text="$store.hovercard.data.reaction_score"></span>
<span class="stat-label">Reactions</span>
</div>
<div class="hovercard-stat">
<span class="stat-value" x-text="$store.hovercard.data.trophy_points"></span>
<span class="stat-label">Points</span>
</div>
<div class="hovercard-stat">
<span class="stat-value" x-text="$store.hovercard.data.entries_count"></span>
<span class="stat-label">Entries</span>
</div>
</div>
<div class="hovercard-actions">
<a href="#" class="btn" title="View profile">
<i data-lucide="user" size="14"></i>
</a>
<a href="#" class="btn" title="Send message">
<i data-lucide="mail" size="14"></i>
</a>
</div>
</div>
</div>
</template>
</div>

View File

@@ -34,7 +34,9 @@
{{ \Auth::user()?->username ?? "Guest" }}
</span>
<span class="user_role">
Lorem
<a href="{{ \Auth::guest() ? xfRoute('login') : xfRoute('logout') }}">
{{ \Auth::guest() ? 'Login' : 'Logout' }}
</a>
</span>
</div>
</div>

View File

@@ -0,0 +1,68 @@
<div
x-data
x-cloak
x-show="$store.notifications.start"
x-translation:enter="dropdown-enter"
x-transition:leave="dropdown-leave"
class="notifications"
@click.outside="$store.notifications.close()"
@keydown.escape.window="$store.notifications.close()"
>
<div class="notifications-header">
<span class="notifications-header-title">Notifications</span>
<div class="notifications-header-actions">
<button type="button" class="btn" x-show="$store.notifications.unread > 0" @click="$store.notifications.markAllRead()" title="Mark all as read">
<i data-lucide="check-circle" size="14"></i>
</button>
<a href="{{ xfRoute('account.alerts') }}" class="btn">
<i data-lucide="external-link" size="14"></i>
</a>
</div>
</div>
<template x-if="$store.notifications.loading">
<div class="notifications-loading">
<i data-lucide="loader-2" class="spin"></i>
</div>
</template>
<template x-if="$store.notifications.error">
<div class="notifications-empty">
<i data-lucide="alert-circle" size="24"></i>
<span>Failed to load notifications.</span>
</div>
</template>
<template x-if="$store.notifications.data && !$store.notifications.loading">
<div>
<template x-if="$store.notifications.data.length === 0">
<div class="notifications-empty">
<i data-lucide="bell-off" size="24"></i>
<span>No new notifications.</span>
</div>
</template>
<template x-for="notif in $store.notifications.data" :key="notif.alert_id">
<a :href="notif.alert_url" class="notifications-item" :class="{ 'unread': notif.view_date === 0 }">
<div class="notifications-avatar">
<template x-if="notif.User?.avatar_urls?.s">
<img :src="notif.User.avatar_urls.s" :alt="notif.username">
</template>
<template x-if="!notif.User?.avatar_urls?.s">
<span x-text="notif.username?.charAt(0).toUpperCase()"></span>
</template>
</div>
<div class="notifications-content">
<span class="notifications-text" x-text="notif.alert_text"></span>
<span class="notifications-date" x-text="new Date(notif.event_date * 1000).toLocaleDateString()"></span>
</div>
<div class="notifications-unread-dot" x-show="notif.view_date === 0"></div>
</a>
</template>
</div>
</template>
</div>

View File

@@ -1,3 +1,4 @@
@php $topbarModSeparator = false; $topBarAdminSeparator = false; @endphp
<header id="topbar">
<button class="mobile-toggle">
<i data-lucide="menu"></i>
@@ -9,8 +10,65 @@
</div>
<div class="topbar-actions">
@if( !\Auth::guest() && \Auth::user()->is_admin === 1 )
@php $topbarAdminSeparator = true; @endphp
<a href="{{ config('app.forum_url') . '/admin.php' }}" class="btn">
<i data-lucide="landmark" size="18"></i>
</a>
<a href="{{ config('app.url') . '/manage' }}" class="btn">
<i data-lucide="shield-cog" size="18"></i>
</a>
@endif
@if( $topbarAdminSeparator )
<div class="vertical-separator"></div>
@endif
@if( !\Auth::guest() && \Auth::user()->is_moderator === 1 )
@php $topbarModSeparator = true; @endphp
<a href="#" class="btn">
<i data-lucide="siren" size="18"></i>
</a>
<a href="{{ xfRoute('approval-queue') }}" class="btn">
<i data-lucide="message-circle-check" size="18"></i>
</a>
<a href="{{ xfRoute('reports') }}" class="btn">
<i data-lucide="triangle-alert" size="18"></i>
</a>
@endif
@if( $topbarModSeparator )
<div class="vertical-separator"></div>
@endif
{{-- Users --}}
@if( !\Auth::guest() && \Auth::user()->can('romhackplaza', 'canSubmitEntry') )
<a href="#" class="btn">
<i data-lucide="hard-drive-upload" size="18"></i>
</a>
@endif
@if( !\Auth::guest() )
<div x-data x-init="$store.notifications.unviewed = {{ \Auth::user()->alerts_unviewed }}" style="position:relative">
<button type="button" class="btn" :class="{ 'active': $store.notifications.start }" @click="$store.notifications.open($el)" @click.outside="$store.notifications.close()">
<i data-lucide="bell" size="18"></i>
<span
class="topbar-badge"
:class="$store.notifications.unread > 9 ? 'topbar-badge--overflow' : ''"
x-show="$store.notifications.unread > 0"
x-text="$store.notifications.unread > 99 ? '99+' : $store.notifications.unread"
></span>
</button>
@include('components.notifications')
</div>
<button class="btn">
<i data-lucide="mail" size="18"></i>
</button>
@endif
<button class="btn">
<i data-lucide="bell" size="18"></i>
<i data-lucide="settings" size="18"></i>
</button>
</div>
</header>

View File

@@ -0,0 +1,6 @@
<span x-data class="userlink"
@mouseenter.debounce.300ms="$store.hovercard.open($el,'{{ route('dynamic.hovercard', ['user_id' => $user?->user_id ?? 0 ]) }}')"
@mouseleave="setTimeout(() => { const C = document.querySelector('.hovercard'); if(!C?.matches(':hover')) $store.hovercard.close(); }, 200)"
>
{{ $user->username }}
</span>