diff --git a/app/Exceptions/InvalidVersionNumberException.php b/app/Exceptions/InvalidVersionNumberException.php new file mode 100644 index 0000000..34ff34d --- /dev/null +++ b/app/Exceptions/InvalidVersionNumberException.php @@ -0,0 +1,10 @@ +builder = $this->baseQuery(); + $this->filters = $filters; + } + + /** + * The base query for the mod listing. + */ + private function baseQuery(): Builder + { + return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at']) + ->withTotalDownloads() + ->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name']) + ->whereHas('latestVersion'); + } + + /** + * Apply the filters to the query. + */ + public function apply(): Builder + { + foreach ($this->filters as $method => $value) { + if (method_exists($this, $method) && ! empty($value)) { + $this->$method($value); + } + } + + return $this->builder; + } + + /** + * Order the query by the given type. + */ + private function order(string $type): Builder + { + // We order the "recently updated" mods by the ModVersion's updated_at value. + if ($type === 'updated') { + return $this->builder->orderByDesc( + ModVersion::select('updated_at') + ->whereColumn('mod_id', 'mods.id') + ->orderByDesc('updated_at') + ->take(1) + ); + } + + // By default, we simply order by the column on the mods table/query. + $column = match ($type) { + 'downloaded' => 'total_downloads', + default => 'created_at', + }; + + return $this->builder->orderByDesc($column); + } + + /** + * Filter the results by the given search term. + */ + private function query(string $term): Builder + { + return $this->builder->whereLike('name', "%$term%"); + } + + /** + * Filter the results by the featured status. + */ + private function featured(string $option): Builder + { + return match ($option) { + 'exclude' => $this->builder->where('featured', false), + 'only' => $this->builder->where('featured', true), + default => $this->builder, + }; + } + + /** + * Filter the results to a specific SPT version. + */ + private function sptVersion(array $versions): Builder + { + return $this->builder->withWhereHas('latestVersion.sptVersion', function ($query) use ($versions) { + $query->whereIn('version', $versions); + $query->orderByDesc('version'); + }); + } +} diff --git a/app/Livewire/Mod/Index.php b/app/Livewire/Mod/Index.php new file mode 100644 index 0000000..34e0ecc --- /dev/null +++ b/app/Livewire/Mod/Index.php @@ -0,0 +1,94 @@ +availableSptVersions = SptVersion::select(['id', 'version', 'color_class'])->orderByDesc('version')->get(); + $this->sptVersion = $this->getLatestMinorVersions()->pluck('version')->toArray(); + } + + /** + * Get all hotfix versions of the latest minor SPT version. + */ + public function getLatestMinorVersions(): Collection + { + return $this->availableSptVersions->filter(function (SptVersion $sptVersion) { + return $sptVersion->isLatestMinor(); + }); + } + + /** + * The component mount method. + */ + public function render(): View + { + // Fetch the mods using the filters saved to the component properties. + $filters = [ + 'query' => $this->query, + 'featured' => $this->featured, + 'order' => $this->order, + 'sptVersion' => $this->sptVersion, + ]; + $mods = (new ModFilter($filters))->apply()->paginate(24); + + return view('livewire.mod.index', compact('mods')); + } + + /** + * The method to reset the filters. + */ + public function resetFilters(): void + { + $this->query = ''; + $this->order = 'created'; + $this->sptVersion = $this->getLatestMinorVersions()->pluck('version')->toArray(); + $this->featured = 'include'; + } +} diff --git a/app/Livewire/ModIndex.php b/app/Livewire/ModIndex.php deleted file mode 100644 index a00370f..0000000 --- a/app/Livewire/ModIndex.php +++ /dev/null @@ -1,71 +0,0 @@ -sectionFilter) { - case 'new': - $section = 'created_at'; - break; - case 'most_downloaded': - $section = 'total_downloads'; - break; - case 'recently_updated': - $section = 'updated_at'; - break; - case 'top_rated': - // probably use some kind of 'likes' or something - // not implemented yet afaik -waffle - break; - } - - $mods = Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at']) - ->withTotalDownloads() - ->with(['users:id,name']) - ->where('name', 'like', '%'.Str::trim($this->modSearch).'%'); - - if ($this->versionFilter === -1) { - $mods = $mods - ->with(['latestVersion', 'latestVersion.sptVersion']) - ->whereHas('latestVersion.sptVersion'); - } else { - $mods = $mods->with(['latestVersion' => function ($query) { - $query->where('spt_version_id', $this->versionFilter); - }, 'latestVersion.sptVersion']) - ->whereHas('latestVersion.sptVersion', function ($query) { - $query->where('spt_version_id', $this->versionFilter); - }); - } - - $mods = $mods->orderByDesc($section)->paginate(12); - - $sptVersions = SptVersion::select(['id', 'version', 'color_class'])->orderByDesc('version')->get(); - - return view('livewire.mod-index', ['mods' => $mods, 'sptVersions' => $sptVersions]); - } - - public function changeSection($section): void - { - $this->sectionFilter = $section; - } -} diff --git a/app/Models/Mod.php b/app/Models/Mod.php index 2388390..b0cd756 100644 --- a/app/Models/Mod.php +++ b/app/Models/Mod.php @@ -34,6 +34,7 @@ class Mod extends Model { // Apply the global scope to exclude disabled mods. static::addGlobalScope(new DisabledScope); + // Apply the global scope to exclude non-published mods. static::addGlobalScope(new PublishedScope); } diff --git a/app/Models/SptVersion.php b/app/Models/SptVersion.php index 0a05dd0..ca66515 100644 --- a/app/Models/SptVersion.php +++ b/app/Models/SptVersion.php @@ -2,10 +2,13 @@ namespace App\Models; +use App\Exceptions\InvalidVersionNumberException; +use App\Services\LatestSptVersionService; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\App; class SptVersion extends Model { @@ -18,4 +21,49 @@ class SptVersion extends Model { return $this->hasMany(ModVersion::class); } + + /** + * Determine if the version is the latest minor version. + */ + public function isLatestMinor(): bool + { + $latestSptVersionService = App::make(LatestSptVersionService::class); + + $latestVersion = $latestSptVersionService->getLatestVersion(); + + if (! $latestVersion) { + return false; + } + + try { + $currentMinorVersion = $this->extractMinorVersion($this->version); + $latestMinorVersion = $this->extractMinorVersion($latestVersion->version); + } catch (InvalidVersionNumberException $e) { + // Could not parse a semver version number. + return false; + } + + return $currentMinorVersion === $latestMinorVersion; + } + + /** + * Extract the minor version from a full version string. + * + * @throws InvalidVersionNumberException + */ + private function extractMinorVersion(string $version): int + { + // Remove everything from the version string except the numbers and dots. + $version = preg_replace('/[^0-9.]/', '', $version); + + // Validate that the version string is a valid semver. + if (! preg_match('/^\d+\.\d+\.\d+$/', $version)) { + throw new InvalidVersionNumberException; + } + + $parts = explode('.', $version); + + // Return the minor version part. + return (int) $parts[1]; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f6f6e91..136e052 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,6 +7,7 @@ use App\Models\ModVersion; use App\Models\User; use App\Observers\ModDependencyObserver; use App\Observers\ModVersionObserver; +use App\Services\LatestSptVersionService; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Number; @@ -19,7 +20,9 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(LatestSptVersionService::class, function ($app) { + return new LatestSptVersionService; + }); } /** diff --git a/app/Services/LatestSptVersionService.php b/app/Services/LatestSptVersionService.php new file mode 100644 index 0000000..eb4e15a --- /dev/null +++ b/app/Services/LatestSptVersionService.php @@ -0,0 +1,19 @@ +version === null) { + $this->version = SptVersion::select('version')->orderByDesc('version')->first(); + } + + return $this->version; + } +} diff --git a/resources/css/app.css b/resources/css/app.css index 7a754ca..839dc3f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -6,7 +6,7 @@ display: none; } -main a:not(.mod-list-component):not(.tab) { +main a:not(.mod-list-component):not(.tab):not([role="menuitem"]) { @apply underline text-gray-800 hover:text-black dark:text-gray-200 dark:hover:text-white; } diff --git a/resources/views/components/filter-checkbox.blade.php b/resources/views/components/filter-checkbox.blade.php new file mode 100644 index 0000000..ba21f90 --- /dev/null +++ b/resources/views/components/filter-checkbox.blade.php @@ -0,0 +1,6 @@ +@props(['id', 'name', 'value']) + +
+ + +
diff --git a/resources/views/components/filter-radio.blade.php b/resources/views/components/filter-radio.blade.php new file mode 100644 index 0000000..754ff46 --- /dev/null +++ b/resources/views/components/filter-radio.blade.php @@ -0,0 +1,6 @@ +@props(['id', 'name', 'value']) + +
+ + +
diff --git a/resources/views/components/filter-sort-menu-item.blade.php b/resources/views/components/filter-sort-menu-item.blade.php new file mode 100644 index 0000000..c64e207 --- /dev/null +++ b/resources/views/components/filter-sort-menu-item.blade.php @@ -0,0 +1,8 @@ +@props(['order', 'currentOrder']) + + + {{ $slot }} + diff --git a/resources/views/components/mod-card.blade.php b/resources/views/components/mod-card.blade.php index 7f4e767..e83c48d 100644 --- a/resources/views/components/mod-card.blade.php +++ b/resources/views/components/mod-card.blade.php @@ -1,5 +1,9 @@ @props(['mod', 'versionScope' => 'latestVersion']) +{{-- + // TODO: Should have some way of visially indicating that a mod is featured. Maybe a sideways ribbon over the thumbnail or something? +--}} +
diff --git a/resources/views/components/mod-list-section-partial.blade.php b/resources/views/components/mod-list-section-partial.blade.php index df9fac3..44e3e11 100644 --- a/resources/views/components/mod-list-section-partial.blade.php +++ b/resources/views/components/mod-list-section-partial.blade.php @@ -1,6 +1,10 @@ -@props(['mods']) +@props(['mods, versionScope, title'])
+ {{-- + TODO: The button-link should be dynamic based on the versionScope. Eg. Featured `View All` button should take + the user to the mods page with the `featured` query parameter set. + --}}
diff --git a/resources/views/livewire/mod-index.blade.php b/resources/views/livewire/mod-index.blade.php deleted file mode 100644 index 21d39d9..0000000 --- a/resources/views/livewire/mod-index.blade.php +++ /dev/null @@ -1,121 +0,0 @@ -
- {{-- page links --}} -
- {{ $mods->links() }} -
- - {{-- grid layout --}} -
- - {{-- column 1 --}} -
- {{-- mods serach bar --}} -
- -
- -
- -
-
- - {{-- mobile section filters --}} -
- -
- - {{-- section filters --}} - - - {{-- mobile version filters --}} -
- -
- - {{-- mobile tags filters --}} -
-

tags filters here when ready :)

-
- - {{-- mod cards --}} -
- @foreach($mods as $mod) - - @endforeach -
-
- - {{-- column 2 --}} - - -
- {{-- page links --}} -
- {{ $mods->links() }} -
- - -
- diff --git a/resources/views/livewire/mod/index.blade.php b/resources/views/livewire/mod/index.blade.php new file mode 100644 index 0000000..8d5912f --- /dev/null +++ b/resources/views/livewire/mod/index.blade.php @@ -0,0 +1,138 @@ +{{-- TODO: This container should have the darker background, like the homepage. --}} +
+

{{ __('Mods') }}

+

{!! __('Explore an enhanced SPT experience with the mods available below. Check out the featured mods for a tailored solo-survival game with maximum immersion.') !!}

+ +
+

{{ __('Filters') }}

+
+
+ + + + +
+ + + +
+ +
+ + + +
+

{{ __('Loading...') }}

+
+
+
+
+
+
+ @php + $totalVersions = count($availableSptVersions); + $half = ceil($totalVersions / 2); + @endphp +
+ {{ __('SPT Versions') }} +
+ @foreach ($availableSptVersions as $index => $version) + @if ($index < $half) + {{ $version->version }} + @endif + @endforeach +
+
+
+   +
+ @foreach ($availableSptVersions as $index => $version) + @if ($index >= $half) + {{ $version->version }} + @endif + @endforeach +
+
+
+
+
+ {{ __('Featured') }} +
+ {{ __('Include') }} + {{ __('Exclude') }} + {{ __('Only') }} +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+ + {{ $mods->onEachSide(1)->links() }} + + {{-- Mod Listing --}} + @if ($mods->isNotEmpty()) +
+ @foreach ($mods as $mod) + + @endforeach +
+ @else +
+

{{ __('There were no mods found with those filters applied. ') }}

+ + + +
+ @endif + + {{ $mods->onEachSide(1)->links() }} + +
diff --git a/resources/views/mod/index.blade.php b/resources/views/mod/index.blade.php index 6df800d..e2030b6 100644 --- a/resources/views/mod/index.blade.php +++ b/resources/views/mod/index.blade.php @@ -1,4 +1,3 @@ - @livewire('mod-index') + @livewire('mod.index') - diff --git a/resources/views/navigation-menu.blade.php b/resources/views/navigation-menu.blade.php index b4b9757..0c626b7 100644 --- a/resources/views/navigation-menu.blade.php +++ b/resources/views/navigation-menu.blade.php @@ -9,9 +9,7 @@
@@ -54,7 +52,15 @@ {{ __('Open user menu') }} {{ auth()->user()->name }} -