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']) + +
tags filters here when ready :)
-{!! __('Explore an enhanced SPT experience with the mods available below. Check out the featured mods for a tailored solo-survival game with maximum immersion.') !!}
+ +{{ __('Loading...') }}
+{{ __('There were no mods found with those filters applied. ') }}
+ +