From a77822256439c40c8d4bc531f209251f65c37b95 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 24 May 2026 11:47:20 +0200 Subject: [PATCH] A lot of things - Added Database page. - Added Xenforo API compatibility - Added Hovercard - Added Notifications --- .gitignore | 3 +- app/Auth/XenForoUser.php | 13 +- .../Controllers/DynamicLoadController.php | 58 ++ app/Http/Controllers/EntryController.php | 6 +- app/Livewire/Database.php | 242 ++++++ app/Services/XenforoApiService.php | 66 ++ app/Services/XenforoService.php | 41 + .../Components/DatabaseFilterWithMode.php | 35 + .../DatabaseFilterWithModeSearch.php | 35 + .../Components/DatabaseFilterWithoutMode.php | 33 + .../DatabaseFilterWithoutModeSearch.php | 33 + app/View/Components/EntryCard.php | 44 + app/View/Components/XfUsernameLink.php | 35 + app/XenForoDataTypes/XenForoData.php | 19 + app/XenForoDataTypes/XenForoUserGroup.php | 7 + config/menu.php | 4 +- config/services.php | 6 + extra.less | 795 +++++++++++++++++- resources/css/app.css | 3 + resources/css/components/cards.css | 75 ++ resources/css/components/common.css | 36 +- resources/css/components/database.css | 391 +++++++++ resources/css/components/grid.css | 8 + resources/css/components/hovercard.css | 119 +++ resources/css/components/notifications.css | 138 +++ resources/css/layout/content.css | 8 + resources/css/xenforo.css | 4 + resources/js/app.js | 8 + resources/js/hovercard.js | 115 +++ resources/js/notifications.js | 126 +++ resources/js/types/AlertsResponseItem.js | 21 + resources/js/types/HovercardResponse.js | 18 + ...database-filter-with-mode-search.blade.php | 32 + .../database-filter-with-mode.blade.php | 26 + ...abase-filter-without-mode-search.blade.php | 28 + .../database-filter-without-mode.blade.php | 22 + .../views/components/entry-card.blade.php | 40 + .../views/components/hovercard.blade.php | 76 ++ resources/views/components/menu.blade.php | 4 +- .../views/components/notifications.blade.php | 68 ++ resources/views/components/topbar.blade.php | 60 +- .../components/xf-username-link.blade.php | 6 + resources/views/entries/index.blade.php | 18 +- resources/views/home.blade.php | 2 +- resources/views/layouts/app.blade.php | 1 + resources/views/livewire/database.blade.php | 79 ++ .../views/vendor/livewire/bootstrap.blade.php | 102 +++ .../livewire/simple-bootstrap.blade.php | 53 ++ .../vendor/livewire/simple-tailwind.blade.php | 56 ++ .../views/vendor/livewire/tailwind.blade.php | 35 + routes/web.php | 13 + 51 files changed, 3228 insertions(+), 38 deletions(-) create mode 100644 app/Http/Controllers/DynamicLoadController.php create mode 100644 app/Livewire/Database.php create mode 100644 app/Services/XenforoApiService.php create mode 100644 app/View/Components/DatabaseFilterWithMode.php create mode 100644 app/View/Components/DatabaseFilterWithModeSearch.php create mode 100644 app/View/Components/DatabaseFilterWithoutMode.php create mode 100644 app/View/Components/DatabaseFilterWithoutModeSearch.php create mode 100644 app/View/Components/EntryCard.php create mode 100644 app/View/Components/XfUsernameLink.php create mode 100644 app/XenForoDataTypes/XenForoData.php create mode 100644 app/XenForoDataTypes/XenForoUserGroup.php create mode 100644 resources/css/components/database.css create mode 100644 resources/css/components/hovercard.css create mode 100644 resources/css/components/notifications.css create mode 100644 resources/css/xenforo.css create mode 100644 resources/js/hovercard.js create mode 100644 resources/js/notifications.js create mode 100644 resources/js/types/AlertsResponseItem.js create mode 100644 resources/js/types/HovercardResponse.js create mode 100644 resources/views/components/database-filter-with-mode-search.blade.php create mode 100644 resources/views/components/database-filter-with-mode.blade.php create mode 100644 resources/views/components/database-filter-without-mode-search.blade.php create mode 100644 resources/views/components/database-filter-without-mode.blade.php create mode 100644 resources/views/components/entry-card.blade.php create mode 100644 resources/views/components/hovercard.blade.php create mode 100644 resources/views/components/notifications.blade.php create mode 100644 resources/views/components/xf-username-link.blade.php create mode 100644 resources/views/livewire/database.blade.php create mode 100644 resources/views/vendor/livewire/bootstrap.blade.php create mode 100644 resources/views/vendor/livewire/simple-bootstrap.blade.php create mode 100644 resources/views/vendor/livewire/simple-tailwind.blade.php create mode 100644 resources/views/vendor/livewire/tailwind.blade.php diff --git a/.gitignore b/.gitignore index 205e3ee..0e5699a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ _ide_helper.php Homestead.json Homestead.yaml Thumbs.db - +avancee.ods +.~lock.avancee.ods# diff --git a/app/Auth/XenForoUser.php b/app/Auth/XenForoUser.php index dc7aec2..8ce1d6f 100644 --- a/app/Auth/XenForoUser.php +++ b/app/Auth/XenForoUser.php @@ -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'; diff --git a/app/Http/Controllers/DynamicLoadController.php b/app/Http/Controllers/DynamicLoadController.php new file mode 100644 index 0000000..7f99545 --- /dev/null +++ b/app/Http/Controllers/DynamicLoadController.php @@ -0,0 +1,58 @@ +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] ); + } +} diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index 3b12399..944dbb7 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -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 diff --git a/app/Livewire/Database.php b/app/Livewire/Database.php new file mode 100644 index 0000000..304de54 --- /dev/null +++ b/app/Livewire/Database.php @@ -0,0 +1,242 @@ + '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(), + ]); + } +} diff --git a/app/Services/XenforoApiService.php b/app/Services/XenforoApiService.php new file mode 100644 index 0000000..041c698 --- /dev/null +++ b/app/Services/XenforoApiService.php @@ -0,0 +1,66 @@ +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 ); + } + +} diff --git a/app/Services/XenforoService.php b/app/Services/XenforoService.php index c16009d..ed12c90 100644 --- a/app/Services/XenforoService.php +++ b/app/Services/XenforoService.php @@ -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. * diff --git a/app/View/Components/DatabaseFilterWithMode.php b/app/View/Components/DatabaseFilterWithMode.php new file mode 100644 index 0000000..60161c5 --- /dev/null +++ b/app/View/Components/DatabaseFilterWithMode.php @@ -0,0 +1,35 @@ + "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'); + } +} diff --git a/app/View/Components/XfUsernameLink.php b/app/View/Components/XfUsernameLink.php new file mode 100644 index 0000000..5580834 --- /dev/null +++ b/app/View/Components/XfUsernameLink.php @@ -0,0 +1,35 @@ +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'); + } +} diff --git a/app/XenForoDataTypes/XenForoData.php b/app/XenForoDataTypes/XenForoData.php new file mode 100644 index 0000000..fc03ac5 --- /dev/null +++ b/app/XenForoDataTypes/XenForoData.php @@ -0,0 +1,19 @@ +services = app(XenforoService::class); + } + + public function __get(string $name): mixed { + return $this->data->$name ?? null; + } + +} diff --git a/app/XenForoDataTypes/XenForoUserGroup.php b/app/XenForoDataTypes/XenForoUserGroup.php new file mode 100644 index 0000000..df02d17 --- /dev/null +++ b/app/XenForoDataTypes/XenForoUserGroup.php @@ -0,0 +1,7 @@ + 'Database', 'icon' => 'database', - 'route' => 'home' + 'route' => 'entries.index' ], [ - 'name' => "Submissions Queue", + 'name' => "Submissions queue", 'icon' => 'gavel', 'route' => 'home' ], diff --git a/config/services.php b/config/services.php index 6a90eb8..7a6199f 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,10 @@ return [ ], ], + 'xf_api' => [ + 'user' => env('XF_API_USER'), + 'key' => env('XF_API_KEY'), + 'url' => env('XF_API_URL'), + ] + ]; diff --git a/extra.less b/extra.less index 1790ddd..cf5ceaf 100644 --- a/extra.less +++ b/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; +} + diff --git a/resources/css/app.css b/resources/css/app.css index 0a672c9..b83671a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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'; diff --git a/resources/css/components/cards.css b/resources/css/components/cards.css index a7dd084..ba5d9f9 100644 --- a/resources/css/components/cards.css +++ b/resources/css/components/cards.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; + } +} diff --git a/resources/css/components/common.css b/resources/css/components/common.css index 34cd862..88cc8ed 100644 --- a/resources/css/components/common.css +++ b/resources/css/components/common.css @@ -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 { diff --git a/resources/css/components/database.css b/resources/css/components/database.css new file mode 100644 index 0000000..c406ad6 --- /dev/null +++ b/resources/css/components/database.css @@ -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); + } +} + diff --git a/resources/css/components/grid.css b/resources/css/components/grid.css index 66c7d30..646de42 100644 --- a/resources/css/components/grid.css +++ b/resources/css/components/grid.css @@ -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; +} + diff --git a/resources/css/components/hovercard.css b/resources/css/components/hovercard.css new file mode 100644 index 0000000..5260bc8 --- /dev/null +++ b/resources/css/components/hovercard.css @@ -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; +} diff --git a/resources/css/components/notifications.css b/resources/css/components/notifications.css new file mode 100644 index 0000000..bfb9a11 --- /dev/null +++ b/resources/css/components/notifications.css @@ -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; +} diff --git a/resources/css/layout/content.css b/resources/css/layout/content.css index 07ff7c2..003f080 100644 --- a/resources/css/layout/content.css +++ b/resources/css/layout/content.css @@ -41,7 +41,15 @@ } } + .topbar-actions { + display: flex; + gap: 8px; + } + .vertical-separator { + align-items: center; + border-left: 1px solid var(--border); + } } diff --git a/resources/css/xenforo.css b/resources/css/xenforo.css new file mode 100644 index 0000000..cee9708 --- /dev/null +++ b/resources/css/xenforo.css @@ -0,0 +1,4 @@ +.xf-menu-user-avatar-fix { + width: 40px !important; + height: 40px !important; +} diff --git a/resources/js/app.js b/resources/js/app.js index 39c1d46..4b7891d 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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() ); diff --git a/resources/js/hovercard.js b/resources/js/hovercard.js new file mode 100644 index 0000000..a66bd8d --- /dev/null +++ b/resources/js/hovercard.js @@ -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} + */ + 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; + } + } +} diff --git a/resources/js/notifications.js b/resources/js/notifications.js new file mode 100644 index 0000000..49d6979 --- /dev/null +++ b/resources/js/notifications.js @@ -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} + */ + 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} + */ + 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} + */ + 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; + } + + } +} diff --git a/resources/js/types/AlertsResponseItem.js b/resources/js/types/AlertsResponseItem.js new file mode 100644 index 0000000..3f86d5f --- /dev/null +++ b/resources/js/types/AlertsResponseItem.js @@ -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 {} diff --git a/resources/js/types/HovercardResponse.js b/resources/js/types/HovercardResponse.js new file mode 100644 index 0000000..730a76e --- /dev/null +++ b/resources/js/types/HovercardResponse.js @@ -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 {} diff --git a/resources/views/components/database-filter-with-mode-search.blade.php b/resources/views/components/database-filter-with-mode-search.blade.php new file mode 100644 index 0000000..44c035c --- /dev/null +++ b/resources/views/components/database-filter-with-mode-search.blade.php @@ -0,0 +1,32 @@ +
+
+
+

{{ $title }}

+ +
+
+
+ + +
+ + +
+
+
+ +
+ @foreach($items as $item) + + @endforeach +
+
+
diff --git a/resources/views/components/database-filter-with-mode.blade.php b/resources/views/components/database-filter-with-mode.blade.php new file mode 100644 index 0000000..0c543bc --- /dev/null +++ b/resources/views/components/database-filter-with-mode.blade.php @@ -0,0 +1,26 @@ +
+
+
+

{{ $title }}

+ +
+
+
+ + +
+ + +
+
+
+ @foreach($items as $item) + + @endforeach +
+
diff --git a/resources/views/components/database-filter-without-mode-search.blade.php b/resources/views/components/database-filter-without-mode-search.blade.php new file mode 100644 index 0000000..03b40e6 --- /dev/null +++ b/resources/views/components/database-filter-without-mode-search.blade.php @@ -0,0 +1,28 @@ +
+
+
+

{{ $title }}

+ +
+
+ + +
+
+
+ +
+ @foreach($items as $item) + + @endforeach +
+
+
diff --git a/resources/views/components/database-filter-without-mode.blade.php b/resources/views/components/database-filter-without-mode.blade.php new file mode 100644 index 0000000..1b82759 --- /dev/null +++ b/resources/views/components/database-filter-without-mode.blade.php @@ -0,0 +1,22 @@ +
+
+
+

{{ $title }}

+ +
+
+ + +
+
+
+ @foreach($items as $item) + + @endforeach +
+
diff --git a/resources/views/components/entry-card.blade.php b/resources/views/components/entry-card.blade.php new file mode 100644 index 0000000..18cc709 --- /dev/null +++ b/resources/views/components/entry-card.blade.php @@ -0,0 +1,40 @@ +
+
+ {{ $entry->getRealPlatform()?->name ?? 'Unknown' }} + @if( $entry->main_image ) + + @else + + @endif +
+
+
{{ $entry->title }}
+
+ @forelse( $entry->authors as $author) + @if($loop->first)By @endif + {{ $author-> name }} + @if( !$loop->last ), @endif + @empty + No authors + @endforelse +
+
+ {{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }} + @if( section_must_be('romhacks', $entry->type ) ) + @foreach( $entry->modifications as $modif ) + {{ $modif->name }} + @endforeach + @if( $entry->status_id ) + {{ $entry->status->name }} + @endif + @foreach( $entry->languages as $lang ) + {{ $lang->name }} + @endforeach + @endif +
+
+ x + Added: {{ $entry->created_at->format('y-m-d') }} +
+
+
diff --git a/resources/views/components/hovercard.blade.php b/resources/views/components/hovercard.blade.php new file mode 100644 index 0000000..d62b801 --- /dev/null +++ b/resources/views/components/hovercard.blade.php @@ -0,0 +1,76 @@ +
+ + + + + + +
diff --git a/resources/views/components/menu.blade.php b/resources/views/components/menu.blade.php index dcdb3ac..6d5b7ec 100644 --- a/resources/views/components/menu.blade.php +++ b/resources/views/components/menu.blade.php @@ -34,7 +34,9 @@ {{ \Auth::user()?->username ?? "Guest" }} - Lorem + + {{ \Auth::guest() ? 'Login' : 'Logout' }} + diff --git a/resources/views/components/notifications.blade.php b/resources/views/components/notifications.blade.php new file mode 100644 index 0000000..294974c --- /dev/null +++ b/resources/views/components/notifications.blade.php @@ -0,0 +1,68 @@ +
+
+ Notifications +
+ + + + +
+
+ + + + + + +
diff --git a/resources/views/components/topbar.blade.php b/resources/views/components/topbar.blade.php index 47b3bbc..2dbde60 100644 --- a/resources/views/components/topbar.blade.php +++ b/resources/views/components/topbar.blade.php @@ -1,3 +1,4 @@ +@php $topbarModSeparator = false; $topBarAdminSeparator = false; @endphp
+ + @include('components.notifications') + + + @endif +
diff --git a/resources/views/components/xf-username-link.blade.php b/resources/views/components/xf-username-link.blade.php new file mode 100644 index 0000000..317129e --- /dev/null +++ b/resources/views/components/xf-username-link.blade.php @@ -0,0 +1,6 @@ + + {{ $user->username }} + diff --git a/resources/views/entries/index.blade.php b/resources/views/entries/index.blade.php index b5cffc2..c0e165b 100644 --- a/resources/views/entries/index.blade.php +++ b/resources/views/entries/index.blade.php @@ -1,11 +1,7 @@ - - - - - RomHack Plaza - - -

Bienvenue sur RomHack Plaza

-

Le catalogue est en construction.

- - +@extends('layouts.app') + +@section('page-title', "Database - " . config('app.name') ) + +@section('content') + @livewire('database') +@endsection diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 3037dbd..c3fa1d9 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -8,5 +8,5 @@ - {{ xfRoute( 'profile-posts.comments', ['profile_post_comment_id' => 1] ) }} + @endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index d8201fa..fd6a2ce 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -28,6 +28,7 @@ + @include('components.hovercard') @livewireScripts @stack('scripts') diff --git a/resources/views/livewire/database.blade.php b/resources/views/livewire/database.blade.php new file mode 100644 index 0000000..835db5a --- /dev/null +++ b/resources/views/livewire/database.blade.php @@ -0,0 +1,79 @@ +
+ +
+ + +
+
+ @foreach( \App\Livewire\Database::SORT_OPTIONS as $k => $v ) + + @if( $sortBy === $k ) + + @endif + @endforeach + {{ $entries->total() }} results +
+ +
+ @forelse($entries as $entry) + + @empty +

No entries found.

+ @endforelse +
+ + {{ $entries->links() }} + +
+
+
diff --git a/resources/views/vendor/livewire/bootstrap.blade.php b/resources/views/vendor/livewire/bootstrap.blade.php new file mode 100644 index 0000000..8316ff9 --- /dev/null +++ b/resources/views/vendor/livewire/bootstrap.blade.php @@ -0,0 +1,102 @@ +@php +if (! isset($scrollTo)) { + $scrollTo = 'body'; +} + +$scrollIntoViewJsSnippet = ($scrollTo !== false) + ? << + @if ($paginator->hasPages()) + + @endif + diff --git a/resources/views/vendor/livewire/simple-bootstrap.blade.php b/resources/views/vendor/livewire/simple-bootstrap.blade.php new file mode 100644 index 0000000..44bdce0 --- /dev/null +++ b/resources/views/vendor/livewire/simple-bootstrap.blade.php @@ -0,0 +1,53 @@ +@php +if (! isset($scrollTo)) { + $scrollTo = 'body'; +} + +$scrollIntoViewJsSnippet = ($scrollTo !== false) + ? << + @if ($paginator->hasPages()) + + @endif + diff --git a/resources/views/vendor/livewire/simple-tailwind.blade.php b/resources/views/vendor/livewire/simple-tailwind.blade.php new file mode 100644 index 0000000..d7f2560 --- /dev/null +++ b/resources/views/vendor/livewire/simple-tailwind.blade.php @@ -0,0 +1,56 @@ +@php +if (! isset($scrollTo)) { + $scrollTo = 'body'; +} + +$scrollIntoViewJsSnippet = ($scrollTo !== false) + ? << + @if ($paginator->hasPages()) + + @endif + diff --git a/resources/views/vendor/livewire/tailwind.blade.php b/resources/views/vendor/livewire/tailwind.blade.php new file mode 100644 index 0000000..c08316e --- /dev/null +++ b/resources/views/vendor/livewire/tailwind.blade.php @@ -0,0 +1,35 @@ +@if ($paginator->hasPages()) +
+ + {{-- Précédent --}} + @if ($paginator->onFirstPage()) + + @else + + @endif + + {{-- Pages --}} + @foreach ($elements as $element) + @if (is_string($element)) + + @endif + + @if (is_array($element)) + @foreach ($element as $page => $url) + + @endforeach + @endif + @endforeach + + {{-- Suivant --}} + @if ($paginator->hasMorePages()) + + @else + + @endif + +
+@endif diff --git a/routes/web.php b/routes/web.php index f948065..ff42d24 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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'); +});