A lot of things
- Added Database page. - Added Xenforo API compatibility - Added Hovercard - Added Notifications
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,4 +25,5 @@ _ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
|
||||
avancee.ods
|
||||
.~lock.avancee.ods#
|
||||
|
||||
@@ -3,21 +3,12 @@
|
||||
namespace App\Auth;
|
||||
|
||||
use App\Services\XenforoService;
|
||||
use App\XenForoDataTypes\XenForoData;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class XenForoUser implements Authenticatable {
|
||||
class XenForoUser extends XenForoData implements Authenticatable {
|
||||
|
||||
public ?array $permissions = null;
|
||||
private XenforoService $services;
|
||||
|
||||
public function __construct(public readonly object $data) {
|
||||
$this->services = app(XenforoService::class);
|
||||
}
|
||||
|
||||
public function __get(string $name): mixed {
|
||||
return $this->data->$name ?? null;
|
||||
}
|
||||
|
||||
public function getAuthIdentifierName(): string
|
||||
{
|
||||
return 'user_id';
|
||||
|
||||
58
app/Http/Controllers/DynamicLoadController.php
Normal file
58
app/Http/Controllers/DynamicLoadController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\XenForoHelpers;
|
||||
use App\Services\XenforoApiService;
|
||||
use App\Services\XenforoService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class DynamicLoadController extends Controller
|
||||
{
|
||||
public function hovercard( Request $request, int $user_id ){
|
||||
|
||||
$data = Cache::remember("xf_hovercard_{$user_id}", 300, function() use($user_id){
|
||||
|
||||
$service = app(XenforoService::class);
|
||||
$user = $service->getXfUser( $user_id );
|
||||
|
||||
if( !$user ){
|
||||
return response()->json(['error' => 'User not found'], 404);
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $user->username,
|
||||
'avatar_url' => $user->getAvatarUrl(),
|
||||
'avatar_color' => XenForoHelpers::getAvatarColor( $user ),
|
||||
'avatar_letter' => XenForoHelpers::getAvatarLetter( $user ),
|
||||
'group_name' => $service->getXfUserGroup( $user?->user_group_id ?? 0 )?->title ?? 'Guest',
|
||||
'joined' => \DateTimeImmutable::createFromTimestamp( $user->register_date ?? 0 )->format('Y-m-d'),
|
||||
'last_seen' => \DateTimeImmutable::createFromTimestamp( $user->last_activity ?? 0 )->format('Y-m-d'),
|
||||
'message_count' => $user->message_count,
|
||||
'reaction_score' => $user->reaction_score,
|
||||
'trophy_points' => $user->trophy_points,
|
||||
'entries_count' => $user->rhpz_entry_count,
|
||||
];
|
||||
|
||||
});
|
||||
|
||||
|
||||
return response()->json( ['user' => $data] );
|
||||
}
|
||||
|
||||
public function getNotifications( Request $request ){
|
||||
|
||||
$service = app(XenforoApiService::class);
|
||||
$data = $service->getUserAlerts(\Auth::user()->user_id);
|
||||
|
||||
return response()->json( $data );
|
||||
}
|
||||
|
||||
public function markAllRead( Request $request ){
|
||||
$service = app(XenforoApiService::class);
|
||||
$service->markAllNotificationsRead(\Auth::user()->user_id);
|
||||
|
||||
return response()->json( ['success' => true] );
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,8 @@ class EntryController extends Controller
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$entries = Entry::published()
|
||||
->with(['game.platform', 'platform'])
|
||||
->latest('published_at')
|
||||
->paginate(30);
|
||||
|
||||
return view('entries.index', compact('entries'));
|
||||
return view('entries.index');
|
||||
}
|
||||
|
||||
public function show(string $section, Entry $entry): View
|
||||
|
||||
242
app/Livewire/Database.php
Normal file
242
app/Livewire/Database.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Author;
|
||||
use App\Models\Entry;
|
||||
use App\Models\Game;
|
||||
use App\Models\Language;
|
||||
use App\Models\Modification;
|
||||
use App\Models\Platform;
|
||||
use App\Models\Status;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Database extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* entry_title search
|
||||
* @var string
|
||||
*/
|
||||
public string $search = '';
|
||||
|
||||
/**
|
||||
* type filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $types = [];
|
||||
|
||||
/**
|
||||
* Games IDs filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $games = [];
|
||||
|
||||
/**
|
||||
* Current game search
|
||||
* @var string
|
||||
*/
|
||||
public string $gameSearch = '';
|
||||
|
||||
/**
|
||||
* Platform IDs filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $platforms = [];
|
||||
|
||||
/**
|
||||
* Status IDs filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $statuses = [];
|
||||
|
||||
/**
|
||||
* Authors IDs filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $authors = [];
|
||||
|
||||
/**
|
||||
* Authors mode and/or.
|
||||
* @var string
|
||||
*/
|
||||
public string $authorsMode = 'or';
|
||||
|
||||
/**
|
||||
* Current author search.
|
||||
* @var string
|
||||
*/
|
||||
public string $authorSearch = '';
|
||||
|
||||
/**
|
||||
* Languages IDs filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $languages = [];
|
||||
|
||||
/**
|
||||
* Languages mode and/or
|
||||
* @var string
|
||||
*/
|
||||
public string $languagesMode = 'or';
|
||||
|
||||
/**
|
||||
* Modifications IDs filter.
|
||||
* @var array
|
||||
*/
|
||||
public array $modifications = [];
|
||||
|
||||
/**
|
||||
* Modifications mode and/or.
|
||||
* @var string
|
||||
*/
|
||||
public string $modificationsMode = 'or';
|
||||
|
||||
/**
|
||||
* Sort by field.
|
||||
* @var string
|
||||
*/
|
||||
public string $sortBy = 'created_at';
|
||||
|
||||
/**
|
||||
* asc/desc
|
||||
* @var string
|
||||
*/
|
||||
public string $sortDir = 'desc';
|
||||
|
||||
/**
|
||||
* Translation of sort options key.
|
||||
*/
|
||||
public const array SORT_OPTIONS = [
|
||||
'created_at' => 'Date added',
|
||||
'release_date' => 'Release date',
|
||||
'title' => 'Title'
|
||||
];
|
||||
|
||||
/**
|
||||
* Translation of entries key.
|
||||
*/
|
||||
public const array ENTRY_TYPES = [
|
||||
'translations' => 'Translations',
|
||||
'romhacks' => 'Romhacks',
|
||||
'homebrew' => 'Homebrew',
|
||||
'utilities' => 'Utilities',
|
||||
'documents' => 'Documents',
|
||||
'lua-scripts' => 'Lua Scripts',
|
||||
'tutorials' => 'Tutorials',
|
||||
];
|
||||
|
||||
public const int PAGINATION = 30;
|
||||
|
||||
public function updatedSearch(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedTypes(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedGames(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedPlatforms(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedStatuses(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedAuthors(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedAuthorsMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedLanguages(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedLanguagesMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedModifications(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedModificationsMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search', 'types', 'platforms', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode'
|
||||
]);
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function setSort(string $field): void
|
||||
{
|
||||
if( $this->sortBy === $field ) {
|
||||
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDir = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
$this->dispatch('filters-updated');
|
||||
}
|
||||
|
||||
private function buildQuery()
|
||||
{
|
||||
$query = Entry::query()->published()->with([
|
||||
'game.platform', 'status', 'authors', 'languages'
|
||||
]);
|
||||
|
||||
if( $this->search ) {
|
||||
$query->where(function($q) {
|
||||
$q->where('title', 'like', '%'.$this->search.'%');
|
||||
$q->orWhere('complete_title', 'like', '%'.$this->search.'%');
|
||||
});
|
||||
}
|
||||
|
||||
if( $this->types ) {
|
||||
$query->whereIn('type', $this->types);
|
||||
}
|
||||
|
||||
if( $this->platforms ) {
|
||||
$query->where(function($q) {
|
||||
$q->whereIn('platform_id', $this->platforms)
|
||||
->orWhereHas('game', fn($q2) => $q2->whereIn('platform_id', $this->platforms) );
|
||||
});
|
||||
}
|
||||
|
||||
if( $this->games ){
|
||||
$query->whereIn('game_id', $this->games);
|
||||
}
|
||||
|
||||
if( $this->statuses ) {
|
||||
$query->whereIn('status_id', $this->statuses);
|
||||
}
|
||||
|
||||
if( $this->authors ) {
|
||||
if( $this->authorsMode === 'and' ) {
|
||||
foreach ( $this->authors as $authorId ) {
|
||||
$query->whereHas('authors', fn($q) => $q->where('authors.id', $authorId));
|
||||
}
|
||||
} else {
|
||||
$query->whereHas('authors', fn($q) => $q->whereIn('authors.id', $this->authors));
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->languages ) {
|
||||
if( $this->languagesMode === 'and' ) {
|
||||
foreach ( $this->languages as $langId ) {
|
||||
$query->whereHas('languages', fn($q) => $q->where('languages.id', $langId));
|
||||
}
|
||||
} else {
|
||||
$query->whereHas('languages', fn($q) => $q->whereIn('languages.id', $this->languages));
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->modifications ) {
|
||||
if( $this->modificationsMode === 'and' ) {
|
||||
foreach ( $this->modifications as $modificationId ) {
|
||||
$query->whereHas('modifications', fn($q) => $q->where('modifications.id', $modificationId));
|
||||
}
|
||||
} else {
|
||||
$query->whereHas('modifications', fn($q) => $q->whereIn('modifications.id', $this->modifications));
|
||||
}
|
||||
}
|
||||
|
||||
return $query->orderBy($this->sortBy, $this->sortDir);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.database', [
|
||||
'entries' => $this->buildQuery()->paginate(self::PAGINATION),
|
||||
'allGames' => Game::orderBy('name')->get(),
|
||||
'allPlatforms' => Platform::orderBy('name')->get(),
|
||||
'allStatuses' => Status::orderBy('name')->get(),
|
||||
'allAuthors' => Author::orderBy('name')->get(),
|
||||
'allLanguages' => Language::orderBy('name')->get(),
|
||||
'allModifications' => Modification::orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
app/Services/XenforoApiService.php
Normal file
66
app/Services/XenforoApiService.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class XenforoApiService {
|
||||
|
||||
private string $apiKey;
|
||||
private int $superUserId;
|
||||
private string $apiUrl;
|
||||
|
||||
public function __construct(){
|
||||
$this->apiKey = config('services.xf_api.key');
|
||||
$this->superUserId = config('services.xf_api.user');
|
||||
$this->apiUrl = config('services.xf_api.url');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
private function get(string $endpoint, ?int $customUserId = null ): mixed
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'XF-Api-Key' => $this->apiKey,
|
||||
'XF-Api-User' => $customUserId ?? $this->superUserId,
|
||||
])->get("{$this->apiUrl}/{$endpoint}");
|
||||
|
||||
if( !$response->ok() )
|
||||
return null;
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
private function post(string $endpoint, ?int $customUserId = null, array $data = [] ): mixed
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'XF-Api-Key' => $this->apiKey,
|
||||
'XF-Api-User' => $customUserId ?? $this->superUserId,
|
||||
])->post("{$this->apiUrl}/{$endpoint}", $data);
|
||||
|
||||
if( !$response->ok() )
|
||||
return null;
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
public function getUserAlerts(int $userId): mixed
|
||||
{
|
||||
if( app(XenforoService::class)->getXfUser($userId)?->alerts_unviewed > 0 )
|
||||
return $this->get("alerts?page=1&cutoff=7days", $userId);
|
||||
|
||||
return Cache::remember("xf_alerts_{$userId}", 60, function() use($userId) {
|
||||
return $this->get("alerts?page=1&cutoff=7days", $userId);
|
||||
});
|
||||
}
|
||||
|
||||
public function markAllNotificationsRead(int $userId): void
|
||||
{
|
||||
Cache::forget("xf_alerts_{$userId}");
|
||||
$this->post("alerts/marl-all", $userId );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Auth\XenForoUser;
|
||||
use App\XenForoDataTypes\XenForoUserGroup;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class XenforoService {
|
||||
@@ -10,6 +12,45 @@ class XenforoService {
|
||||
private const int TTL_PERMISSIONS = 300;
|
||||
private const int TTL_ROUTES = 86400;
|
||||
|
||||
/**
|
||||
* Get specific XenForo user.
|
||||
*
|
||||
* @param int $xfUserId
|
||||
*
|
||||
* @return XenForoUser|null
|
||||
*/
|
||||
public function getXfUser( int $xfUserId ): ?XenForoUser {
|
||||
|
||||
$xfUser = \DB::connection('xenforo')
|
||||
->table('user')
|
||||
->where('user_id', $xfUserId)
|
||||
->first();
|
||||
|
||||
if(!$xfUser)
|
||||
return null;
|
||||
|
||||
return new XenForoUser($xfUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific XenForo user group.
|
||||
*
|
||||
* @param int $xfUserGroupId
|
||||
*
|
||||
* @return XenForoUserGroup|null
|
||||
*/
|
||||
public function getXfUserGroup( int $xfUserGroupId ): ?XenForoUserGroup {
|
||||
$xfUserGroup = \DB::connection('xenforo')
|
||||
->table('user_group')
|
||||
->where('user_group_id', $xfUserGroupId)
|
||||
->first();
|
||||
|
||||
if(!$xfUserGroup)
|
||||
return null;
|
||||
|
||||
return new XenForoUserGroup($xfUserGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions for a specific user ID.
|
||||
*
|
||||
|
||||
35
app/View/Components/DatabaseFilterWithMode.php
Normal file
35
app/View/Components/DatabaseFilterWithMode.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class DatabaseFilterWithMode extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public $items,
|
||||
public string $model,
|
||||
public string $modeModel,
|
||||
public string $selectedMode,
|
||||
|
||||
public string $idProperty = 'id',
|
||||
public string $nameProperty = 'name',
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.database-filter-with-mode');
|
||||
}
|
||||
}
|
||||
35
app/View/Components/DatabaseFilterWithModeSearch.php
Normal file
35
app/View/Components/DatabaseFilterWithModeSearch.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class DatabaseFilterWithModeSearch extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public $items,
|
||||
public string $model,
|
||||
public string $modeModel,
|
||||
public string $selectedMode,
|
||||
|
||||
public string $idProperty = 'id',
|
||||
public string $nameProperty = 'name',
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.database-filter-with-mode-search');
|
||||
}
|
||||
}
|
||||
33
app/View/Components/DatabaseFilterWithoutMode.php
Normal file
33
app/View/Components/DatabaseFilterWithoutMode.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class DatabaseFilterWithoutMode extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public $items,
|
||||
public string $model,
|
||||
|
||||
public string $idProperty = 'id',
|
||||
public string $nameProperty = 'name',
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.database-filter-without-mode');
|
||||
}
|
||||
}
|
||||
33
app/View/Components/DatabaseFilterWithoutModeSearch.php
Normal file
33
app/View/Components/DatabaseFilterWithoutModeSearch.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class DatabaseFilterWithoutModeSearch extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public $items,
|
||||
public string $model,
|
||||
|
||||
public string $idProperty = 'id',
|
||||
public string $nameProperty = 'name',
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.database-filter-without-mode-search');
|
||||
}
|
||||
}
|
||||
44
app/View/Components/EntryCard.php
Normal file
44
app/View/Components/EntryCard.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Models\Entry;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class EntryCard extends Component
|
||||
{
|
||||
|
||||
/**
|
||||
* Acronym for entry badge.
|
||||
* TODO: Add in common.css other colors.
|
||||
*/
|
||||
public const array ENTRY_TYPES_BADGE = [
|
||||
'translations' => "Trans",
|
||||
'romhacks' => 'Hack',
|
||||
'homebrew' => 'HBrew',
|
||||
'utilities' => 'Util',
|
||||
'documents' => 'Doc',
|
||||
'lua-scripts' => 'Lua',
|
||||
'tutorials' => 'Tuto'
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Entry $entry,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.entry-card');
|
||||
}
|
||||
}
|
||||
35
app/View/Components/XfUsernameLink.php
Normal file
35
app/View/Components/XfUsernameLink.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Auth\XenForoUser;
|
||||
use App\Services\XenforoService;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class XfUsernameLink extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public ?XenForoUser $user = null,
|
||||
public ?int $userId = null
|
||||
)
|
||||
{
|
||||
|
||||
if( $this->user === null && $this->userId !== null ){
|
||||
$this->user = app(XenforoService::class)->getXfUser($this->userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.xf-username-link');
|
||||
}
|
||||
}
|
||||
19
app/XenForoDataTypes/XenForoData.php
Normal file
19
app/XenForoDataTypes/XenForoData.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\XenForoDataTypes;
|
||||
|
||||
use App\Services\XenforoService;
|
||||
|
||||
class XenForoData {
|
||||
|
||||
protected XenforoService $services;
|
||||
|
||||
public function __construct(public readonly object $data) {
|
||||
$this->services = app(XenforoService::class);
|
||||
}
|
||||
|
||||
public function __get(string $name): mixed {
|
||||
return $this->data->$name ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
7
app/XenForoDataTypes/XenForoUserGroup.php
Normal file
7
app/XenForoDataTypes/XenForoUserGroup.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\XenForoDataTypes;
|
||||
|
||||
class XenForoUserGroup extends XenForoData {
|
||||
|
||||
}
|
||||
@@ -16,10 +16,10 @@ return [
|
||||
[
|
||||
'name' => 'Database',
|
||||
'icon' => 'database',
|
||||
'route' => 'home'
|
||||
'route' => 'entries.index'
|
||||
],
|
||||
[
|
||||
'name' => "Submissions Queue",
|
||||
'name' => "Submissions queue",
|
||||
'icon' => 'gavel',
|
||||
'route' => 'home'
|
||||
],
|
||||
|
||||
@@ -35,4 +35,10 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'xf_api' => [
|
||||
'user' => env('XF_API_USER'),
|
||||
'key' => env('XF_API_KEY'),
|
||||
'url' => env('XF_API_URL'),
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
795
extra.less
795
extra.less
@@ -98,6 +98,81 @@ ul {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* ENTRY CARDS */
|
||||
|
||||
.\$entry-card {
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
|
||||
.\$entry-cover-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
background-color: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.\$entry-cover-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.\$entry-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid var(--border);
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.\$entry-card-info {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.\$entry-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.\$entry-card-author {
|
||||
color: var(--rhpz-orange);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.\$entry-card-meta {
|
||||
margin-top: auto;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* File: resources/css/components/common.css */
|
||||
/* BUTTONS */
|
||||
@@ -189,17 +264,50 @@ ul {
|
||||
color: var(--text3);
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
.\$badge.blue {
|
||||
.\$badge.blue, .\$badge.translations {
|
||||
background-color: var(--info);
|
||||
color: var(--text);
|
||||
border-color: var(--info);
|
||||
}
|
||||
.\$badge.green {
|
||||
.\$badge.green, .\$badge.romhacks {
|
||||
background-color: var(--success2);
|
||||
color: var(--text);
|
||||
border-color: var(--success2);
|
||||
}
|
||||
|
||||
.\$topbar-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
border-radius: 9px;
|
||||
background-color: var(--rhpz-orange);
|
||||
color: #111;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
border: 2px solid var(--bg);
|
||||
animation: badge-pop 0.2s ease;
|
||||
}
|
||||
|
||||
.\$topbar-badge--overflow {
|
||||
border-radius: 9px;
|
||||
padding: 0 5px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
@keyframes badge-pop {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
70% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* BREADCRUMB */
|
||||
|
||||
.\$breadcrumb {
|
||||
@@ -248,6 +356,400 @@ ul {
|
||||
}
|
||||
|
||||
|
||||
/* File: resources/css/components/database.css */
|
||||
.\$filter-bar {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
background-color: var(--bg2);
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.\$filter-bar-search {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.\$database-wrapper {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
|
||||
.\$database-filters {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
.\$filter-group {
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background-color: var(--bg3);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.\$filter-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--text2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.\$filter-mode {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.\$filter-btn-mode {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
cursor: pointer;
|
||||
font-family: var(--typography);
|
||||
transition: all 0.15s;
|
||||
letter-spacing: 0.5px;
|
||||
&:hover {
|
||||
border-color: var(--rhpz-orange);
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
&.\$active {
|
||||
background-color: var(--rhpz-orange);
|
||||
border-color: var(--rhpz-orange);
|
||||
color: var(--text3);
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-options {
|
||||
padding: 6px 0;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg2);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 6px 14px;
|
||||
font-size: 0.88rem;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s;
|
||||
&:hover {
|
||||
background-color: var(--bg4);
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-option input[type="checkbox"] {
|
||||
accent-color: var(--rhpz-orange);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.\$database-results {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.\$database-sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
|
||||
.\$btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
&.\$active {
|
||||
border-color: var(--rhpz-orange);
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.\$database-results-count {
|
||||
margin-left: auto;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.\$database-active-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.\$database-active-filter-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 3px 10px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text);
|
||||
.\$tag-type {
|
||||
color: var(--rhpz-orange);
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color 0.15s;
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.\$database-empty {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text2);
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
color: var(--border);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.\$database-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 20px;
|
||||
|
||||
.\$btn {
|
||||
min-width: 36px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.\$active {
|
||||
background-color: var(--rhpz-orange);
|
||||
border-color: var(--rhpz-orange);
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.\$database-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.\$database-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.\$database-filters {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2,1fr);
|
||||
}
|
||||
|
||||
.\$database-filter-group:last-child {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.\$database-results-count {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.\$database-filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.\$grid-entries {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.\$grid-entries {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-chevron {
|
||||
transition: transform 0.2s ease;
|
||||
color: var(--text2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.\$filter-chevron.rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.\$internal-filter-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background-color: var(--bg);
|
||||
|
||||
i {
|
||||
color: var(--text2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text);
|
||||
font-family: var(--typography);
|
||||
font-size: 0.85rem;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.\$filter-title-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.\$internal-filter-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--rhpz-orange);
|
||||
color: #111;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.\$internal-filter-clear {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
.\$filter-search-clear {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s;
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* File: resources/css/components/easymde.css */
|
||||
.\$EasyMDEContainer {
|
||||
display: flex;
|
||||
@@ -864,10 +1366,140 @@ ul {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.\$grid-entries {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6,1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* File: resources/css/components/hovercard.css */
|
||||
.\$hovercard-overlay {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.\$hovercard-overlay-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.\$hovercard-overlay-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.\$hovercard {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.\$hovercard-header {
|
||||
height: 70px;
|
||||
background-color: var(--bg3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.\$hovercard-avatar {
|
||||
position: absolute;
|
||||
bottom: -26px;
|
||||
left: 16px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--bg2);
|
||||
overflow: hidden;
|
||||
background-color: var(--bg4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.\$hovercard-body {
|
||||
padding: 34px 16px 16px;
|
||||
}
|
||||
|
||||
.\$hovercard-username {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.\$hovercard-title {
|
||||
font-size: 0.8rem;
|
||||
color: var(--rhpz-orange);
|
||||
margin-bottom: 14px;
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
.\$hovercard-stats {
|
||||
display: flex;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.\$hovercard-stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 4px;
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.\$stat-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.\$stat-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.\$hovercard-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.\$hovercard-actions .\$btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
|
||||
/* File: resources/css/components/modal.css */
|
||||
.\$modal-overlay {
|
||||
display: none;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
@@ -921,6 +1553,148 @@ ul {
|
||||
}
|
||||
|
||||
|
||||
/* File: resources/css/components/notifications.css */
|
||||
.\$notifications {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 340px;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
z-index: 2000;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border);
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--bg2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-enter {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.\$dropdown-enter {
|
||||
animation: dropdown-enter 0.15s ease;
|
||||
}
|
||||
|
||||
.\$notifications-header {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg3);
|
||||
z-index: 1;
|
||||
|
||||
.\$notifications-header-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.\$notifications-header-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.\$notifications-loading, .\$notifications-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 40px 20px;
|
||||
color: var(--text2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.\$notifications-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: background-color 0.1s;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--bg3);
|
||||
}
|
||||
.\$unread {
|
||||
border-left: 2px solid var(--rhpz-orange);
|
||||
background-color: var(--bg3);
|
||||
}
|
||||
}
|
||||
|
||||
.\$notifications-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.\$notifications-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.\$notifications-text {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.\$notifications-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
}
|
||||
|
||||
.\$notifications-unread-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--rhpz-orange);
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
||||
/* File: resources/css/layout/content.css */
|
||||
#main-wrapper {
|
||||
flex-grow: 1;
|
||||
@@ -965,7 +1739,15 @@ ul {
|
||||
}
|
||||
}
|
||||
|
||||
.\$topbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.\$vertical-separator {
|
||||
align-items: center;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1292,3 +2074,10 @@ ul {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* File: resources/css/xenforo.css */
|
||||
.\$xf-menu-user-avatar-fix {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
@import './components/cards.css';
|
||||
@import './components/modal.css';
|
||||
@import './components/files.css';
|
||||
@import './components/database.css';
|
||||
@import './components/hovercard.css';
|
||||
@import './components/notifications.css';
|
||||
|
||||
@import './components/easymde.css';
|
||||
|
||||
|
||||
@@ -22,3 +22,78 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* ENTRY CARDS */
|
||||
|
||||
.entry-card {
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
|
||||
.entry-cover-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
background-color: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entry-cover-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.entry-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid var(--border);
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.entry-card-info {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entry-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.entry-card-author {
|
||||
color: var(--rhpz-orange);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.entry-card-meta {
|
||||
margin-top: auto;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,17 +87,49 @@
|
||||
color: var(--text3);
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
.badge.blue {
|
||||
.badge.blue, .badge.translations {
|
||||
background-color: var(--info);
|
||||
color: var(--text);
|
||||
border-color: var(--info);
|
||||
}
|
||||
.badge.green {
|
||||
.badge.green, .badge.romhacks {
|
||||
background-color: var(--success2);
|
||||
color: var(--text);
|
||||
border-color: var(--success2);
|
||||
}
|
||||
|
||||
.topbar-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
border-radius: 9px;
|
||||
background-color: var(--rhpz-orange);
|
||||
color: #111;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
border: 2px solid var(--bg);
|
||||
animation: badge-pop 0.2s ease;
|
||||
}
|
||||
|
||||
.topbar-badge--overflow {
|
||||
border-radius: 9px;
|
||||
padding: 0 5px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
@keyframes badge-pop {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
70% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* BREADCRUMB */
|
||||
|
||||
.breadcrumb {
|
||||
|
||||
391
resources/css/components/database.css
Normal file
391
resources/css/components/database.css
Normal file
@@ -0,0 +1,391 @@
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
background-color: var(--bg2);
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.filter-bar-search {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.database-wrapper {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
|
||||
.database-filters {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
.filter-group {
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background-color: var(--bg3);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.filter-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--text2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.filter-mode {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filter-btn-mode {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
cursor: pointer;
|
||||
font-family: var(--typography);
|
||||
transition: all 0.15s;
|
||||
letter-spacing: 0.5px;
|
||||
&:hover {
|
||||
border-color: var(--rhpz-orange);
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--rhpz-orange);
|
||||
border-color: var(--rhpz-orange);
|
||||
color: var(--text3);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
padding: 6px 0;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg2);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 6px 14px;
|
||||
font-size: 0.88rem;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s;
|
||||
&:hover {
|
||||
background-color: var(--bg4);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-option input[type="checkbox"] {
|
||||
accent-color: var(--rhpz-orange);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.database-results {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.database-sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
&.active {
|
||||
border-color: var(--rhpz-orange);
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.database-results-count {
|
||||
margin-left: auto;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.database-active-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.database-active-filter-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 3px 10px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text);
|
||||
.tag-type {
|
||||
color: var(--rhpz-orange);
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color 0.15s;
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.database-empty {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text2);
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
color: var(--border);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.database-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 20px;
|
||||
|
||||
.btn {
|
||||
min-width: 36px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--rhpz-orange);
|
||||
border-color: var(--rhpz-orange);
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.database-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.database-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-filters {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2,1fr);
|
||||
}
|
||||
|
||||
.database-filter-group:last-child {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.database-results-count {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.database-filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-entries {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.grid-entries {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-chevron {
|
||||
transition: transform 0.2s ease;
|
||||
color: var(--text2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-chevron.rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.internal-filter-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background-color: var(--bg);
|
||||
|
||||
i {
|
||||
color: var(--text2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text);
|
||||
font-family: var(--typography);
|
||||
font-size: 0.85rem;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.filter-title-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.internal-filter-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--rhpz-orange);
|
||||
color: #111;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.internal-filter-clear {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-search-clear {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s;
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,3 +27,11 @@
|
||||
grid-template-columns: 0.5fr 1fr 0.25fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-entries {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6,1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
119
resources/css/components/hovercard.css
Normal file
119
resources/css/components/hovercard.css
Normal file
@@ -0,0 +1,119 @@
|
||||
.hovercard-overlay {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.hovercard-overlay-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.hovercard-overlay-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.hovercard {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.hovercard-header {
|
||||
height: 70px;
|
||||
background-color: var(--bg3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hovercard-avatar {
|
||||
position: absolute;
|
||||
bottom: -26px;
|
||||
left: 16px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--bg2);
|
||||
overflow: hidden;
|
||||
background-color: var(--bg4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.hovercard-body {
|
||||
padding: 34px 16px 16px;
|
||||
}
|
||||
|
||||
.hovercard-username {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.hovercard-title {
|
||||
font-size: 0.8rem;
|
||||
color: var(--rhpz-orange);
|
||||
margin-bottom: 14px;
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
.hovercard-stats {
|
||||
display: flex;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.hovercard-stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 4px;
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hovercard-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hovercard-actions .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
138
resources/css/components/notifications.css
Normal file
138
resources/css/components/notifications.css
Normal file
@@ -0,0 +1,138 @@
|
||||
.notifications {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 340px;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
z-index: 2000;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border);
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--bg2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-enter {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.dropdown-enter {
|
||||
animation: dropdown-enter 0.15s ease;
|
||||
}
|
||||
|
||||
.notifications-header {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg3);
|
||||
z-index: 1;
|
||||
|
||||
.notifications-header-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.notifications-header-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-loading, .notifications-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 40px 20px;
|
||||
color: var(--text2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.notifications-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: background-color 0.1s;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--bg3);
|
||||
}
|
||||
.unread {
|
||||
border-left: 2px solid var(--rhpz-orange);
|
||||
background-color: var(--bg3);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.notifications-text {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notifications-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-unread-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--rhpz-orange);
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -41,7 +41,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vertical-separator {
|
||||
align-items: center;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
4
resources/css/xenforo.css
Normal file
4
resources/css/xenforo.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.xf-menu-user-avatar-fix {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import EasyMDE from "easymde";
|
||||
import "easymde/dist/easymde.min.css";
|
||||
|
||||
import { calculate as calculateHashes } from "./hashes.js";
|
||||
import hovercard from "./hovercard.js";
|
||||
import notifications from "./notifications.js";
|
||||
|
||||
|
||||
// Lucide icons.
|
||||
@@ -19,3 +21,9 @@ window.EasyMDE = EasyMDE;
|
||||
|
||||
// Hashes.
|
||||
window.calculateHashes = calculateHashes;
|
||||
|
||||
// Hover card.
|
||||
Alpine.store('hovercard', hovercard() );
|
||||
|
||||
// Notifications
|
||||
Alpine.store('notifications', notifications() );
|
||||
|
||||
115
resources/js/hovercard.js
Normal file
115
resources/js/hovercard.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/** @typedef { import('types/HovercardResponse.js').HovercardResponse} HovercardResponse */
|
||||
|
||||
export default function hovercard(){
|
||||
return {
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
start: false,
|
||||
|
||||
/**
|
||||
* @type {HovercardResponse}
|
||||
*/
|
||||
data: null,
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
loading: false,
|
||||
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
error: false,
|
||||
|
||||
/**
|
||||
* @type {HTMLElement|null}
|
||||
*/
|
||||
anchorEl: null,
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
x: 0,
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
y: 0,
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} anchorEl
|
||||
* @param {string} fetchUrl
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async open(anchorEl, fetchUrl){
|
||||
|
||||
if( this.start && this.anchorEl === anchorEl ){
|
||||
// this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.start = true;
|
||||
this.anchorEl = anchorEl;
|
||||
this.data = null;
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
this.updatePosition(anchorEl);
|
||||
|
||||
try {
|
||||
const RESPONSE = await fetch(fetchUrl);
|
||||
if( !RESPONSE.ok )
|
||||
throw new Error(RESPONSE.status);
|
||||
|
||||
let json = await RESPONSE.json();
|
||||
if( !json.user )
|
||||
throw new Error(RESPONSE.status);
|
||||
|
||||
this.data = json.user;
|
||||
|
||||
Alpine.nextTick(() => {
|
||||
const card = document.querySelector('.hovercard');
|
||||
if (card) window.refreshIcons(card);
|
||||
});
|
||||
|
||||
} catch( error ){
|
||||
this.error = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Hovercard position
|
||||
* @param {HTMLElement} anchorEl
|
||||
*/
|
||||
updatePosition(anchorEl){
|
||||
const RECT = anchorEl.getBoundingClientRect();
|
||||
const SCROLL_X = window.scrollX;
|
||||
const SCROLL_Y = window.scrollY;
|
||||
|
||||
let x = RECT.left + SCROLL_X;
|
||||
let y = RECT.bottom + SCROLL_Y + 8;
|
||||
|
||||
const WIDTH = 280;
|
||||
if( x + WIDTH > window.innerWidth ){
|
||||
x = window.innerWidth - WIDTH - 16;
|
||||
}
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the hovercard.
|
||||
*/
|
||||
close(){
|
||||
this.start = false;
|
||||
this.data = null;
|
||||
this.anchorEl = null;
|
||||
this.error = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
resources/js/notifications.js
Normal file
126
resources/js/notifications.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/** @typedef { import('types/AlertsResponseItem.js').AlertsResponseItem} AlertsResponseItem */
|
||||
|
||||
export default function notifications() {
|
||||
return {
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
start: false,
|
||||
|
||||
/**
|
||||
* @type {AlertsResponseItem[]}
|
||||
*/
|
||||
data: null,
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
loading: false,
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
error: false,
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
unviewed: 0,
|
||||
|
||||
/**
|
||||
* Request for getting notifications.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async getNotifications() {
|
||||
if( this.loading )
|
||||
return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
|
||||
try {
|
||||
const RESPONSE = await fetch('/api/dynamic/notifications', { credentials: "include", headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
|
||||
if( !RESPONSE.ok )
|
||||
throw new Error(RESPONSE.status)
|
||||
|
||||
let json = await RESPONSE.json()
|
||||
if( !json.alerts )
|
||||
throw new Error(RESPONSE.status);
|
||||
|
||||
this.data = json.alerts;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
this.error = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} anchorEl
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async open( anchorEl ){
|
||||
|
||||
if( this.start ){
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.start = !this.start;
|
||||
if( this.start && !this.data ){
|
||||
await this.getNotifications();
|
||||
}
|
||||
|
||||
if( this.start ){
|
||||
Alpine.nextTick(() => window.refreshIcons(document.querySelector('.notifications')))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async markAllRead(){
|
||||
await fetch('/api/dynamic/notifications/mark-all-read', {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '' }
|
||||
});
|
||||
|
||||
if(this.data && this.data.length > 0){
|
||||
this.data = this.data.map(a => ({
|
||||
...a,
|
||||
view_date: Math.floor(Date.now() / 1000)
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
get unread(){
|
||||
if( !this.data ){
|
||||
return this.unviewed;
|
||||
}
|
||||
return this.data.filter(a => a.view_date === 0 ).length;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
close(){
|
||||
|
||||
if( this.start && this.unread > 0)
|
||||
this.markAllRead();
|
||||
|
||||
this.start = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
21
resources/js/types/AlertsResponseItem.js
Normal file
21
resources/js/types/AlertsResponseItem.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @typedef {Object} AlertsResponseItem
|
||||
*
|
||||
* @see app/Http/DynamicLoadController.php
|
||||
*
|
||||
* @property {number} alert_id
|
||||
* @property {number} alerted_user_id
|
||||
* @property {number} user_id
|
||||
* @property {string} username
|
||||
* @property {string} content_type
|
||||
* @property {number} content_id
|
||||
* @property {string} action
|
||||
* @property {number} event_date
|
||||
* @property {number} view_date
|
||||
* @property {number} read_date
|
||||
* @property {boolean} auto_read
|
||||
* @property {string} alert_text
|
||||
* @property {string} alert_url
|
||||
* @property {Object} user See XenForo API Documentation for more details.
|
||||
*/
|
||||
export {}
|
||||
18
resources/js/types/HovercardResponse.js
Normal file
18
resources/js/types/HovercardResponse.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {Object} HovercardResponse
|
||||
*
|
||||
* @see app/Http/DynamicLoadController.php
|
||||
*
|
||||
* @property {string} username
|
||||
* @property {string|null} avatar_url
|
||||
* @property {string} avatar_color
|
||||
* @property {string} avatar_letter
|
||||
* @property {string} group_name
|
||||
* @property {string} joined
|
||||
* @property {string} last_seen
|
||||
* @property {number} message_count
|
||||
* @property {number} reaction_score
|
||||
* @property {number} trophy_points
|
||||
* @property {number} entries_count
|
||||
*/
|
||||
export {}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
40
resources/views/components/entry-card.blade.php
Normal file
40
resources/views/components/entry-card.blade.php
Normal 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>
|
||||
76
resources/views/components/hovercard.blade.php
Normal file
76
resources/views/components/hovercard.blade.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
68
resources/views/components/notifications.blade.php
Normal file
68
resources/views/components/notifications.blade.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
6
resources/views/components/xf-username-link.blade.php
Normal file
6
resources/views/components/xf-username-link.blade.php
Normal 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>
|
||||
@@ -1,11 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>RomHack Plaza</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Bienvenue sur RomHack Plaza</h1>
|
||||
<p>Le catalogue est en construction.</p>
|
||||
</body>
|
||||
</html>
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', "Database - " . config('app.name') )
|
||||
|
||||
@section('content')
|
||||
@livewire('database')
|
||||
@endsection
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
</div>
|
||||
<x-error-block error-type="page-not-allowed" />
|
||||
|
||||
{{ xfRoute( 'profile-posts.comments', ['profile_post_comment_id' => 1] ) }}
|
||||
<x-xf-username-link user-id="2" />
|
||||
@endsection
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</main>
|
||||
|
||||
</div>
|
||||
@include('components.hovercard')
|
||||
@livewireScripts
|
||||
@stack('scripts')
|
||||
</body>
|
||||
|
||||
79
resources/views/livewire/database.blade.php
Normal file
79
resources/views/livewire/database.blade.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<div @filters-updated.window="refreshIcons($el)">
|
||||
<div class="database-search filter-bar">
|
||||
<input
|
||||
type="text"
|
||||
wire:model.live.debounce="search"
|
||||
placeholder="Search..."
|
||||
class="form-input filter-bar-search"
|
||||
>
|
||||
@if( $search || $types || $platforms || $games || $statuses || $authors || $languages || $modifications )
|
||||
<button type="button" wire:click="clearFilters" class="btn">
|
||||
<i data-lucide="x"></i> Clear filters
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="database-wrapper">
|
||||
<aside class="database-filters">
|
||||
|
||||
{{-- Types --}}
|
||||
<div class="filter-group" x-data="{open: true}">
|
||||
<div class="filter-title-row" @click="open = !open">
|
||||
<h4 class="filter-title">Type</h4>
|
||||
<i data-lucide="chevron-down" size="14" class="filter-chevron" :class="{ 'rotated': !open }"></i>
|
||||
</div>
|
||||
<div class="filter-options" x-show="open" x-transition>
|
||||
@foreach( \App\Livewire\Database::ENTRY_TYPES as $k => $v )
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" wire:model.live="types" value="{{ $k }}">
|
||||
{{ $v }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Games --}}
|
||||
<x-database-filter-without-mode-search title="Game" :items="$allGames" model="games" />
|
||||
|
||||
{{-- Platforms --}}
|
||||
<x-database-filter-without-mode title="Platform" :items="$allPlatforms" model="platforms"/>
|
||||
|
||||
{{-- Statuses --}}
|
||||
<x-database-filter-without-mode title="Status" :items="$allStatuses" model="statuses"/>
|
||||
|
||||
{{-- Authors --}}
|
||||
<x-database-filter-with-mode-search title="Authors" :items="$allAuthors" model="authors" mode-model="authorsMode" :selected-mode="$authorsMode" />
|
||||
|
||||
{{-- Languages --}}
|
||||
<x-database-filter-with-mode title="Languages" :items="$allLanguages" model="languages" mode-model="languagesMode" :selected-mode="$languagesMode" />
|
||||
|
||||
{{-- Modifications --}}
|
||||
<x-database-filter-with-mode title="Modifications" :items="$allModifications" model="modifications" mode-model="modificationsMode" :selected-mode="$modificationsMode" />
|
||||
|
||||
</aside>
|
||||
|
||||
<div class="database-results">
|
||||
<div class="database-sort">
|
||||
@foreach( \App\Livewire\Database::SORT_OPTIONS as $k => $v )
|
||||
<button type="button" wire:click="setSort('{{ $k }}')" class="btn {{ $sortBy === $k ? 'active' : '' }}">
|
||||
{{ $v }}
|
||||
</button>
|
||||
@if( $sortBy === $k )
|
||||
<i data-lucide="{{ $sortDir === 'asc' ? 'arrow-up' : 'arrow-down' }}"></i>
|
||||
@endif
|
||||
@endforeach
|
||||
<span class="database-results-count">{{ $entries->total() }} results</span>
|
||||
</div>
|
||||
|
||||
<div class="grid-entries">
|
||||
@forelse($entries as $entry)
|
||||
<x-entry-card :entry="$entry" />
|
||||
@empty
|
||||
<p>No entries found.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $entries->links() }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
102
resources/views/vendor/livewire/bootstrap.blade.php
vendored
Normal file
102
resources/views/vendor/livewire/bootstrap.blade.php
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav class="d-flex justify-items-center justify-content-between">
|
||||
<div class="d-flex justify-content-between flex-fill d-sm-none">
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.previous')</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link" aria-hidden="true">@lang('pagination.next')</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-none flex-sm-fill d-sm-flex align-items-sm-center justify-content-sm-between">
|
||||
<div>
|
||||
<p class="small text-muted">
|
||||
{!! __('Showing') !!}
|
||||
<span class="fw-semibold">{{ $paginator->firstItem() }}</span>
|
||||
{!! __('to') !!}
|
||||
<span class="fw-semibold">{{ $paginator->lastItem() }}</span>
|
||||
{!! __('of') !!}
|
||||
<span class="fw-semibold">{{ $paginator->total() }}</span>
|
||||
{!! __('results') !!}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
|
||||
<span class="page-link" aria-hidden="true">‹</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.previous')">‹</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Pagination Elements --}}
|
||||
@foreach ($elements as $element)
|
||||
{{-- "Three Dots" Separator --}}
|
||||
@if (is_string($element))
|
||||
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
|
||||
@endif
|
||||
|
||||
{{-- Array Of Links --}}
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<li class="page-item active" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}" aria-current="page"><span class="page-link">{{ $page }}</span></li>
|
||||
@else
|
||||
<li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}">{{ $page }}</button></li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.next')">›</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
|
||||
<span class="page-link" aria-hidden="true">›</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
53
resources/views/vendor/livewire/simple-bootstrap.blade.php
vendored
Normal file
53
resources/views/vendor/livewire/simple-bootstrap.blade.php
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.previous')</span>
|
||||
</li>
|
||||
@else
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<li class="page-item">
|
||||
<button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<li class="page-item">
|
||||
<button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@endif
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.next')</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
56
resources/views/vendor/livewire/simple-tailwind.blade.php
vendored
Normal file
56
resources/views/vendor/livewire/simple-tailwind.blade.php
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav role="navigation" aria-label="Pagination Navigation" class="flex justify-between">
|
||||
<span>
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.previous') !!}
|
||||
</span>
|
||||
@else
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@else
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.next') !!}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
35
resources/views/vendor/livewire/tailwind.blade.php
vendored
Normal file
35
resources/views/vendor/livewire/tailwind.blade.php
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
@if ($paginator->hasPages())
|
||||
<div class="database-pagination">
|
||||
|
||||
{{-- Précédent --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<button class="btn" disabled>«</button>
|
||||
@else
|
||||
<button class="btn" wire:click="previousPage">«</button>
|
||||
@endif
|
||||
|
||||
{{-- Pages --}}
|
||||
@foreach ($elements as $element)
|
||||
@if (is_string($element))
|
||||
<button class="btn" disabled>{{ $element }}</button>
|
||||
@endif
|
||||
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
<button
|
||||
class="btn {{ $page == $paginator->currentPage() ? 'active' : '' }}"
|
||||
wire:click="gotoPage({{ $page }})"
|
||||
>{{ $page }}</button>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Suivant --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<button class="btn" wire:click="nextPage">»</button>
|
||||
@else
|
||||
<button class="btn" disabled>»</button>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
@@ -34,6 +34,8 @@ Route::name('submit.')->prefix('/edit')->controller(\App\Http\Controllers\Submis
|
||||
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', 'entry' => '[0-9\-]+' ]);
|
||||
});
|
||||
|
||||
/* API ROUTES */
|
||||
|
||||
// FileServerController
|
||||
Route::name('fs.')->controller(\App\Http\Controllers\FileServerController::class)->group(function () {
|
||||
Route::post('/api/fs/upload-chunk/{section}', 'uploadChunk' )->name('uploadchunk')
|
||||
@@ -50,3 +52,14 @@ Route::name('tempfile.')->controller(\App\Http\Controllers\TemporaryFileControll
|
||||
Route::post('/api/tempfile/upload', 'upload' )->name('upload')
|
||||
->middleware('xf.auth:romhackplaza.canSubmitTempFile');
|
||||
});
|
||||
|
||||
// DynamicLoadController
|
||||
Route::get( '/api/dynamic/hovercard/{user_id}', [ \App\Http\Controllers\DynamicLoadController::class, 'hovercard' ] )
|
||||
->where(['user_id' => '[0-9]+'])
|
||||
->name('dynamic.hovercard')
|
||||
->middleware('throttle:60,1')
|
||||
;
|
||||
Route::middleware('xf.auth')->controller(\App\Http\Controllers\DynamicLoadController::class)->name('dynamic.')->prefix('/api/dynamic/')->group(function(){
|
||||
Route::get('/notifications', 'getNotifications' )->name('notifications');
|
||||
Route::post('/notifications/mark-all-read', 'markAllRead' )->name('markallread');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user