diff --git a/.env.example b/.env.example
index dbbfe5d..540da35 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,4 @@
-APP_NAME=Laravel
+APP_NAME="The Forge"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
@@ -12,6 +12,7 @@ APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
+# Much higher in production.
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
@@ -19,19 +20,22 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
-DB_CONNECTION=sqlite
-# DB_HOST=127.0.0.1
-# DB_PORT=3306
-# DB_DATABASE=laravel
-# DB_USERNAME=root
-# DB_PASSWORD=
+# Only 'mysql' and 'pgsql' are supported due to a 'naturalsort' database function.
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=laravel
+DB_USERNAME=root
+DB_PASSWORD=
-DB_WOLTLAB_CONNECTION=sqlite
-# DB_WOLTLAB_HOST=127.0.0.1
-# DB_WOLTLAB_PORT=3306
-# DB_WOLTLAB_DATABASE=laravel
-# DB_WOLTLAB_USERNAME=root
-# DB_WOLTLAB_PASSWORD=
+# This is only needed if you are running the app:import-woltlab-data command.
+# For normal development you should just seed the database with fake data.
+DB_WOLTLAB_CONNECTION=mysql
+DB_WOLTLAB_HOST=127.0.0.1
+DB_WOLTLAB_PORT=3306
+DB_WOLTLAB_DATABASE=laravel
+DB_WOLTLAB_USERNAME=root
+DB_WOLTLAB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
diff --git a/app/Console/Commands/ImportWoltlabData.php b/app/Console/Commands/ImportWoltlabData.php
index 26b3c01..6585731 100644
--- a/app/Console/Commands/ImportWoltlabData.php
+++ b/app/Console/Commands/ImportWoltlabData.php
@@ -9,6 +9,7 @@ use App\Models\SptVersion;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
+use Illuminate\Support\Benchmark;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
@@ -33,21 +34,60 @@ class ImportWoltlabData extends Command
*/
public function handle(): void
{
- $this->importUsers();
+ $this->newLine();
- $this->importLicenses();
+ $totalTime = Benchmark::value(function () {
+ $loadDataTime = Benchmark::value(function () {
+ $this->loadData();
+ });
+ $this->info('Execution time: '.round($loadDataTime[1], 2).'ms');
+ $this->newLine();
- $this->importSptVersions();
+ $importUsersTime = Benchmark::value(function () {
+ $this->importUsers();
+ });
+ $this->info('Execution time: '.round($importUsersTime[1], 2).'ms');
+ $this->newLine();
+ $importLicensesTime = Benchmark::value(function () {
+ $this->importLicenses();
+ });
+ $this->info('Execution time: '.round($importLicensesTime[1], 2).'ms');
+ $this->newLine();
+
+ $importSptVersionsTime = Benchmark::value(function () {
+ $this->importSptVersions();
+ });
+ $this->info('Execution time: '.round($importSptVersionsTime[1], 2).'ms');
+ $this->newLine();
+
+ $importModsTime = Benchmark::value(function () {
+ $this->importMods();
+ });
+ $this->info('Execution time: '.round($importModsTime[1], 2).'ms');
+ $this->newLine();
+
+ $importModVersionsTime = Benchmark::value(function () {
+ $this->importModVersions();
+ });
+ $this->info('Execution time: '.round($importModVersionsTime[1], 2).'ms');
+ $this->newLine();
+ });
+
+ $this->newLine();
+ $this->info('Data imported successfully');
+ $this->info('Total execution time: '.round($totalTime[1], 2).'ms');
+ }
+
+ protected function loadData(): void
+ {
+ // We're just going to dump a few things in memory to escape the N+1 problem.
+ $this->output->write('Loading data into memory... ');
$this->fileOptionValues = $this->getFileOptionValues();
$this->fileContent = $this->getFileContent();
- $this->importMods();
-
- $this->fileVersionContent = $this->getFileVersionContent();
$this->fileVersionLabels = $this->getFileVersionLabels();
- $this->importModVersions();
-
- $this->info('Data imported successfully.');
+ $this->fileVersionContent = $this->getFileVersionContent();
+ $this->info('Done.');
}
protected function importUsers(): void
@@ -84,7 +124,6 @@ class ImportWoltlabData extends Command
}, 'userID');
$this->info('Total users processed: '.$totalInserted);
- $this->newLine();
}
protected function importLicenses(): void
@@ -112,7 +151,6 @@ class ImportWoltlabData extends Command
}, 'licenseID');
$this->info('Total licenses processed: '.$totalInserted);
- $this->newLine();
}
protected function importSptVersions(): void
@@ -125,7 +163,7 @@ class ImportWoltlabData extends Command
$insertData[] = [
'hub_id' => $version->labelID,
'version' => $version->label,
- 'color_class' => $version->cssClassName,
+ 'color_class' => $this->translateColour($version->cssClassName),
];
}
@@ -140,7 +178,17 @@ class ImportWoltlabData extends Command
}, 'labelID');
$this->info('Total licenses processed: '.$totalInserted);
- $this->newLine();
+ }
+
+ protected function translateColour(string $colour = ''): string
+ {
+ return match ($colour) {
+ 'green' => 'green',
+ 'slightly-outdated' => 'lime',
+ 'yellow' => 'yellow',
+ 'red' => 'red',
+ default => 'gray',
+ };
}
protected function importMods(): void
@@ -155,20 +203,20 @@ class ImportWoltlabData extends Command
DB::connection('mysql_woltlab')->table('filebase1_file')->chunkById(100, function (Collection $mods) use (&$command, &$curl, &$totalInserted) {
foreach ($mods as $mod) {
- if ($mod->fileID == 1 || $mod->fileID == 116 || $mod->fileID == 480) {
- // These are special cases that we don't want to import. Installers, base files, and the patchers.
- continue;
- }
-
$modContent = $this->fileContent[$mod->fileID] ?? [];
$modOptions = $this->fileOptionValues[$mod->fileID] ?? [];
+ $versionLabel = $this->fileVersionLabels[$mod->fileID] ?? [];
+
+ if (empty($versionLabel)) {
+ continue;
+ }
$insertData[] = [
'hub_id' => $mod->fileID,
'user_id' => User::whereHubId($mod->userID)->value('id'),
'name' => $modContent ? $modContent->subject : '',
'slug' => $modContent ? Str::slug($modContent->subject) : '',
- 'teaser' => $modContent ? $modContent->teaser : '',
+ 'teaser' => $modContent ? (strlen($modContent->teaser) > 100 ? Str::take($modContent->teaser, 97).'...' : $modContent->teaser) : '',
'description' => $modContent ? $modContent->message : '',
'thumbnail' => $this->fetchModThumbnail($command, $curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
@@ -194,7 +242,6 @@ class ImportWoltlabData extends Command
curl_close($curl);
$this->info('Total mods processed: '.$totalInserted);
- $this->newLine();
}
protected function getFileOptionValues(): array
@@ -331,7 +378,7 @@ class ImportWoltlabData extends Command
$command = $this;
$totalInserted = 0;
- DB::connection('mysql_woltlab')->table('filebase1_file_version')->chunkById(100, function (Collection $versions) use (&$command, &$totalInserted) {
+ DB::connection('mysql_woltlab')->table('filebase1_file_version')->chunkById(500, function (Collection $versions) use (&$command, &$totalInserted) {
foreach ($versions as $version) {
$versionContent = $this->fileVersionContent[$version->versionID] ?? [];
@@ -369,7 +416,6 @@ class ImportWoltlabData extends Command
}, 'versionID');
$this->info('Total mod versions processed: '.$totalInserted);
- $this->newLine();
}
protected function fetchVirusTotalLink(array $options): string
diff --git a/app/Helpers/ColorHelper.php b/app/Helpers/ColorHelper.php
deleted file mode 100644
index eed2f1e..0000000
--- a/app/Helpers/ColorHelper.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'bg-red-50 text-red-700 ring-red-600/20',
- 'green' => 'bg-green-50 text-green-700 ring-green-600/20',
- 'yellow' => 'bg-yellow-50 text-yellow-700 ring-yellow-600/20',
- default => 'bg-gray-50 text-gray-700 ring-gray-600/20',
- };
- }
-}
diff --git a/app/Models/Mod.php b/app/Models/Mod.php
index 727f327..9115583 100644
--- a/app/Models/Mod.php
+++ b/app/Models/Mod.php
@@ -23,6 +23,14 @@ class Mod extends Model
'source_code_link',
];
+ protected function slug(): Attribute
+ {
+ return Attribute::make(
+ get: fn (string $value) => strtolower($value),
+ set: fn (string $value) => Str::slug($value),
+ );
+ }
+
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
@@ -38,21 +46,54 @@ class Mod extends Model
return $this->hasMany(ModVersion::class);
}
- public function versionLastUpdated()
+ public function scopeWithTotalDownloads($query)
{
- return $this->hasOne(ModVersion::class)->lastUpdated();
+ $query->addSelect(['total_downloads' => ModVersion::selectRaw('SUM(downloads) AS total_downloads')
+ ->whereColumn('mod_id', 'mods.id'),
+ ]);
}
- public function versionLatestSptVersion()
+ public function latestSptVersion(): BelongsTo
{
- return $this->hasOne(ModVersion::class)->latestSptVersion();
+ return $this->belongsTo(ModVersion::class, 'latest_spt_version_id');
}
- protected function slug(): Attribute
+ public function scopeWithLatestSptVersion($query)
{
- return Attribute::make(
- get: fn (string $value) => strtolower($value),
- set: fn (string $value) => Str::slug($value),
- );
+ return $query
+ ->addSelect(['latest_spt_version_id' => ModVersion::select('id')
+ ->whereColumn('mod_id', 'mods.id')
+ ->orderByDesc(
+ SptVersion::select('version')
+ ->whereColumn('mod_versions.spt_version_id', 'spt_versions.id')
+ ->orderByDesc('version')
+ ->take(1),
+ )
+ ->orderByDesc('version')
+ ->take(1),
+ ])
+ ->with(['latestSptVersion', 'latestSptVersion.sptVersion']);
+ }
+
+ public function lastUpdatedVersion(): BelongsTo
+ {
+ return $this->belongsTo(ModVersion::class, 'last_updated_spt_version_id');
+ }
+
+ public function scopeWithLastUpdatedVersion($query)
+ {
+ return $query
+ ->addSelect(['last_updated_spt_version_id' => ModVersion::select('id')
+ ->whereColumn('mod_id', 'mods.id')
+ ->orderByDesc('updated_at')
+ ->take(1),
+ ])
+ ->orderByDesc(
+ ModVersion::select('updated_at')
+ ->whereColumn('mod_id', 'mods.id')
+ ->orderByDesc('updated_at')
+ ->take(1)
+ )
+ ->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion']);
}
}
diff --git a/app/Models/ModVersion.php b/app/Models/ModVersion.php
index 4ee7ac1..52e62b0 100644
--- a/app/Models/ModVersion.php
+++ b/app/Models/ModVersion.php
@@ -2,7 +2,6 @@
namespace App\Models;
-use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -26,20 +25,8 @@ class ModVersion extends Model
return $this->belongsTo(Mod::class);
}
- public function sptVersion(): belongsTo
+ public function sptVersion(): BelongsTo
{
return $this->belongsTo(SptVersion::class);
}
-
- public function scopeLastUpdated(Builder $query): void
- {
- $query->orderByDesc('created_at');
- }
-
- public function scopeLatestSptVersion(Builder $query): void
- {
- $query->orderByDesc(
- SptVersion::select('spt_versions.version')->whereColumn('mod_versions.spt_version_id', 'spt_versions.id')
- )->orderByDesc('mod_versions.version');
- }
}
diff --git a/app/View/Components/ModList.php b/app/View/Components/ModList.php
index 715ae58..6d46793 100644
--- a/app/View/Components/ModList.php
+++ b/app/View/Components/ModList.php
@@ -2,13 +2,13 @@
namespace App\View\Components;
-use App\Helpers\ColorHelper;
use Illuminate\Contracts\View\View;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\View\Component;
class ModList extends Component
{
- public $mods;
+ public Collection $mods;
public string $versionScope;
@@ -16,11 +16,6 @@ class ModList extends Component
{
$this->mods = $mods;
$this->versionScope = $versionScope;
-
- foreach ($this->mods as $mod) {
- $color = $mod->{$this->versionScope}->sptVersion->color_class;
- $mod->colorClass = ColorHelper::tagColorClasses($color);
- }
}
public function render(): View
diff --git a/app/View/Components/ModListSection.php b/app/View/Components/ModListSection.php
index fe20bbf..8dc2f9b 100644
--- a/app/View/Components/ModListSection.php
+++ b/app/View/Components/ModListSection.php
@@ -24,17 +24,36 @@ class ModListSection extends Component
private function fetchFeaturedMods(): Collection
{
- return Mod::with('versionLatestSptVersion.sptVersion')->whereFeatured(true)->take(6)->get();
+ return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
+ ->withLatestSptVersion()
+ ->withTotalDownloads()
+ ->with('user:id,name')
+ ->where('featured', true)
+ ->latest()
+ ->limit(6)
+ ->get();
}
private function fetchLatestMods(): Collection
{
- return Mod::with('versionLatestSptVersion.sptVersion')->latest()->take(6)->get();
+ return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
+ ->withLatestSptVersion()
+ ->withTotalDownloads()
+ ->with('user:id,name')
+ ->latest()
+ ->limit(6)
+ ->get();
}
private function fetchUpdatedMods(): Collection
{
- return Mod::with('versionLastUpdated.sptVersion')->take(6)->get();
+ return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
+ ->withLastUpdatedVersion()
+ ->withTotalDownloads()
+ ->with('user:id,name')
+ ->latest()
+ ->limit(6)
+ ->get();
}
public function getSections(): array
@@ -43,17 +62,17 @@ class ModListSection extends Component
[
'title' => 'Featured Mods',
'mods' => $this->modsFeatured,
- 'versionScope' => 'versionLatestSptVersion',
+ 'versionScope' => 'latestSptVersion',
],
[
- 'title' => 'Latest Mods',
+ 'title' => 'Newest Mods',
'mods' => $this->modsLatest,
- 'versionScope' => 'versionLatestSptVersion',
+ 'versionScope' => 'latestSptVersion',
],
[
'title' => 'Recently Updated Mods',
'mods' => $this->modsUpdated,
- 'versionScope' => 'versionLastUpdated',
+ 'versionScope' => 'lastUpdatedVersion',
],
];
}
diff --git a/app/View/Components/ModVersionTag.php b/app/View/Components/ModVersionTag.php
deleted file mode 100644
index 69947e4..0000000
--- a/app/View/Components/ModVersionTag.php
+++ /dev/null
@@ -1,31 +0,0 @@
-tagColor = $tagColor;
- }
-
- public function render(): View
- {
- return view('components.mod-version-tag');
- }
-
- public function tagClasses($tagColor): string
- {
- return match ($this->tagColor) {
- 'red' => 'bg-red-50 text-red-700 ring-red-600/20',
- 'green' => 'bg-green-50 text-green-700 ring-green-600/20',
- 'yellow' => 'bg-yellow-50 text-yellow-700 ring-yellow-600/20',
- default => 'bg-gray-50 text-gray-700 ring-gray-600/20',
- };
- }
-}
diff --git a/composer.json b/composer.json
index 56e2f3c..64cee15 100644
--- a/composer.json
+++ b/composer.json
@@ -33,10 +33,7 @@
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
- },
- "files": [
- "app/Helpers/ColorHelper.php"
- ]
+ }
},
"autoload-dev": {
"psr-4": {
diff --git a/config/database.php b/config/database.php
index 016efe6..450f87e 100644
--- a/config/database.php
+++ b/config/database.php
@@ -16,7 +16,7 @@ return [
|
*/
- 'default' => env('DB_CONNECTION', 'sqlite'),
+ 'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php
index 4d36f1f..3b49aca 100644
--- a/database/migrations/0001_01_01_000000_create_users_table.php
+++ b/database/migrations/0001_01_01_000000_create_users_table.php
@@ -13,7 +13,7 @@ return new class extends Migration
{
Schema::create('users', function (Blueprint $table) {
$table->id();
- $table->string('hub_id')->nullable()->unique()->default(null);
+ $table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
diff --git a/database/migrations/2024_05_15_022315_create_licenses_table.php b/database/migrations/2024_05_15_022315_create_licenses_table.php
index 033d0d0..0d2bf75 100644
--- a/database/migrations/2024_05_15_022315_create_licenses_table.php
+++ b/database/migrations/2024_05_15_022315_create_licenses_table.php
@@ -10,7 +10,7 @@ return new class extends Migration
{
Schema::create('licenses', function (Blueprint $table) {
$table->id();
- $table->string('hub_id')->nullable()->unique()->default(null);
+ $table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->string('name');
$table->string('link');
$table->softDeletes();
diff --git a/database/migrations/2024_05_15_022710_create_mods_table.php b/database/migrations/2024_05_15_022710_create_mods_table.php
index d03e6dc..fbb6f05 100644
--- a/database/migrations/2024_05_15_022710_create_mods_table.php
+++ b/database/migrations/2024_05_15_022710_create_mods_table.php
@@ -12,7 +12,7 @@ return new class extends Migration
{
Schema::create('mods', function (Blueprint $table) {
$table->id();
- $table->string('hub_id')->nullable()->unique()->default(null);
+ $table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->foreignIdFor(User::class)->constrained('users');
$table->string('name');
$table->string('slug');
diff --git a/database/migrations/2024_05_15_023430_create_spt_versions_table.php b/database/migrations/2024_05_15_023430_create_spt_versions_table.php
index 502e059..091cdc5 100644
--- a/database/migrations/2024_05_15_023430_create_spt_versions_table.php
+++ b/database/migrations/2024_05_15_023430_create_spt_versions_table.php
@@ -10,7 +10,7 @@ return new class extends Migration
{
Schema::create('spt_versions', function (Blueprint $table) {
$table->id();
- $table->string('hub_id')->nullable()->unique()->default(null);
+ $table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->string('version');
$table->string('color_class');
$table->softDeletes();
diff --git a/database/migrations/2024_05_15_023705_create_mod_versions_table.php b/database/migrations/2024_05_15_023705_create_mod_versions_table.php
index 6aa8090..ef83e58 100644
--- a/database/migrations/2024_05_15_023705_create_mod_versions_table.php
+++ b/database/migrations/2024_05_15_023705_create_mod_versions_table.php
@@ -12,7 +12,7 @@ return new class extends Migration
{
Schema::create('mod_versions', function (Blueprint $table) {
$table->id();
- $table->string('hub_id')->nullable()->unique()->default(null);
+ $table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->foreignIdFor(Mod::class)->constrained('mods');
$table->string('version');
$table->longText('description');
diff --git a/database/migrations/2024_05_24_132251_create_natural_sort_function.php b/database/migrations/2024_05_24_132251_create_natural_sort_function.php
new file mode 100644
index 0000000..22ce8a8
--- /dev/null
+++ b/database/migrations/2024_05_24_132251_create_natural_sort_function.php
@@ -0,0 +1,79 @@
+
-
{{ $mod->description }}
-{{ $mod->teaser }}
++ By {{ $mod->user->name }} + @if (!is_null($mod->total_downloads)) + – {{ Number::format($mod->total_downloads) }} downloads + @endif + @if(!is_null($mod->updated_at)) + – updated {{ $mod->updated_at }} + @endif +
Notice: The Forge is currently under +
Notice: The Forge is currently under heavy construction. Expect nothing to work.
The greatest resource available for Single Player Tarkov modifications. Where modding legends are made. Discover powerful tools, expert-written guides, and exclusive mods. Craft your vision. Transform the game.
-The greatest resource available for Single Player Tarkov modifications. Where modding legends are made. Discover powerful tools, expert-written guides, and exclusive mods. Craft your vision. Transform the game.
+{{ $mod->description }}
-