mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
Updates to search
This commit is contained in:
parent
054c458b0f
commit
92b04eb286
58
.env.example
58
.env.example
@ -3,14 +3,12 @@ APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_URL=http://forge.test
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
APP_MAINTENANCE_STORE=database
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# Much higher in production.
|
||||
BCRYPT_ROUNDS=12
|
||||
@ -21,23 +19,25 @@ LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_DATABASE=forge
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# This is only needed if you are running the app:import-woltlab-data command.
|
||||
# This is only needed if you are running the app:import-hub command.
|
||||
# For normal development you should just seed the database with fake data:
|
||||
# `php artisan migrate:fresh --seed`
|
||||
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=
|
||||
DB_HUB_CONNECTION=mysql
|
||||
DB_HUB_HOST=localhost
|
||||
DB_HUB_PORT=3306
|
||||
DB_HUB_DATABASE=forge
|
||||
DB_HUB_USERNAME=root
|
||||
DB_HUB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_STORE=redis
|
||||
SESSION_CONNECTION=default
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
@ -45,36 +45,30 @@ SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
CACHE_PREFIX=
|
||||
CACHE_STORE=redis
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
REDIS_CACHE_CONNECTION=cache
|
||||
REDIS_QUEUE_CONNECTION=queue
|
||||
REDIS_QUEUE=queue
|
||||
|
||||
SCOUT_DRIVER=meilisearch
|
||||
SCOUT_QUEUE=true
|
||||
|
||||
MEILISEARCH_HOST=http://127.0.0.1:7700
|
||||
MEILISEARCH_KEY=FORGE
|
||||
MEILISEARCH_KEY=LARAVEL-HERD
|
||||
MEILISEARCH_NO_ANALYTICS=true
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_USERNAME=${APP_NAME}
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
NOVA_LICENSE_KEY=
|
||||
|
487
app/Console/Commands/ImportHub.php
Normal file
487
app/Console/Commands/ImportHub.php
Normal file
@ -0,0 +1,487 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
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;
|
||||
use Illuminate\Support\Str;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
|
||||
class ImportHub extends Command
|
||||
{
|
||||
protected $signature = 'app:import-hub';
|
||||
|
||||
protected $description = 'Connects to the Hub database and imports the data into the Laravel database.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
// This may take a minute or two...
|
||||
set_time_limit(0);
|
||||
|
||||
$this->newLine();
|
||||
|
||||
$totalTime = Benchmark::value(function () {
|
||||
$loadDataTime = Benchmark::value(function () {
|
||||
$this->loadData();
|
||||
});
|
||||
$this->info('Execution time: '.round($loadDataTime[1], 2).'ms');
|
||||
$this->newLine();
|
||||
|
||||
$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();
|
||||
});
|
||||
|
||||
// Disconnect from the Hub database, clearing temporary tables.
|
||||
DB::connection('mysql_hub')->disconnect();
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Data imported successfully');
|
||||
$this->info('Total execution time: '.round($totalTime[1], 2).'ms');
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Refreshing Meilisearch indexes...');
|
||||
$this->call('scout:delete-all-indexes');
|
||||
$this->call('scout:sync-index-settings');
|
||||
$this->call('scout:import', ['model' => '\App\Models\Mod']);
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Done');
|
||||
}
|
||||
|
||||
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->bringFileOptionsLocal();
|
||||
$this->bringFileContentLocal();
|
||||
$this->bringFileVersionLabelsLocal();
|
||||
$this->bringFileVersionContentLocal();
|
||||
$this->info('Done.');
|
||||
}
|
||||
|
||||
protected function importUsers(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
|
||||
foreach (DB::connection('mysql_hub')->table('wcf1_user')->orderBy('userID')->cursor() as $wolt) {
|
||||
$registrationDate = Carbon::parse($wolt->registrationDate, 'UTC');
|
||||
if ($registrationDate->isFuture()) {
|
||||
$registrationDate = now('UTC');
|
||||
}
|
||||
$registrationDate->setTimezone('UTC');
|
||||
|
||||
$insertData = [
|
||||
'hub_id' => $wolt->userID,
|
||||
'name' => $wolt->username,
|
||||
'email' => mb_convert_case($wolt->email, MB_CASE_LOWER, 'UTF-8'),
|
||||
'password' => $this->cleanPasswordHash($wolt->password),
|
||||
'created_at' => $registrationDate,
|
||||
'updated_at' => now('UTC')->toDateTimeString(),
|
||||
];
|
||||
|
||||
User::upsert($insertData, ['hub_id'], ['name', 'email', 'password', 'created_at', 'updated_at']);
|
||||
$totalInserted++;
|
||||
|
||||
// Log every 2500 users processed
|
||||
if ($totalInserted % 2500 == 0) {
|
||||
$this->line('Processed 2500 users. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Total users processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function cleanPasswordHash(string $password): string
|
||||
{
|
||||
// The hub passwords are hashed sometimes with a prefix of the hash type. We only want the hash.
|
||||
// If it's not Bcrypt, they'll have to reset their password. Tough luck.
|
||||
return str_replace(['Bcrypt:', 'cryptMD5:', 'cryptMD5::'], '', $password);
|
||||
}
|
||||
|
||||
protected function importLicenses(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_license')
|
||||
->chunkById(100, function (Collection $licenses) use (&$totalInserted) {
|
||||
$insertData = [];
|
||||
foreach ($licenses as $license) {
|
||||
$insertData[] = [
|
||||
'hub_id' => $license->licenseID,
|
||||
'name' => $license->licenseName,
|
||||
'link' => $license->licenseURL,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
DB::table('licenses')->upsert($insertData, ['hub_id'], ['name', 'link']);
|
||||
$totalInserted += count($insertData);
|
||||
$this->line('Processed '.count($insertData).' licenses. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($licenses);
|
||||
}, 'licenseID');
|
||||
|
||||
$this->info('Total licenses processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function importSptVersions(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_label')
|
||||
->where('groupID', 1)
|
||||
->chunkById(100, function (Collection $versions) use (&$totalInserted) {
|
||||
$insertData = [];
|
||||
foreach ($versions as $version) {
|
||||
$insertData[] = [
|
||||
'hub_id' => $version->labelID,
|
||||
'version' => $version->label,
|
||||
'color_class' => $this->translateColour($version->cssClassName),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
DB::table('spt_versions')->upsert($insertData, ['hub_id'], ['version', 'color_class']);
|
||||
$totalInserted += count($insertData);
|
||||
$this->line('Processed '.count($insertData).' SPT Versions. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($versions);
|
||||
}, 'labelID');
|
||||
|
||||
$this->info('Total licenses processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function translateColour(string $colour = ''): string
|
||||
{
|
||||
return match ($colour) {
|
||||
'green' => 'green',
|
||||
'slightly-outdated' => 'lime',
|
||||
'yellow' => 'yellow',
|
||||
'red' => 'red',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
protected function importMods(): void
|
||||
{
|
||||
$command = $this;
|
||||
$totalInserted = 0;
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file')
|
||||
->chunkById(100, function (Collection $mods) use (&$command, &$curl, &$totalInserted) {
|
||||
|
||||
foreach ($mods as $mod) {
|
||||
|
||||
$modContent = DB::table('temp_file_content')
|
||||
->where('fileID', $mod->fileID)
|
||||
->orderBy('fileID', 'desc')
|
||||
->first();
|
||||
|
||||
$optionSourceCode = DB::table('temp_file_option_values')
|
||||
->select('optionValue as source_code_link')
|
||||
->where('fileID', $mod->fileID)
|
||||
->whereIn('optionID', [5, 1])
|
||||
->whereNot('optionValue', '')
|
||||
->orderByDesc('optionID')
|
||||
->first();
|
||||
|
||||
$optionContainsAi = DB::table('temp_file_option_values')
|
||||
->select('optionValue as contains_ai')
|
||||
->where('fileID', $mod->fileID)
|
||||
->where('optionID', 7)
|
||||
->whereNot('optionValue', '')
|
||||
->first();
|
||||
|
||||
$optionContainsAds = DB::table('temp_file_option_values')
|
||||
->select('optionValue as contains_ads')
|
||||
->where('fileID', $mod->fileID)
|
||||
->where('optionID', 3)
|
||||
->whereNot('optionValue', '')
|
||||
->first();
|
||||
|
||||
$versionLabel = DB::table('temp_file_version_labels')
|
||||
->select('labelID')
|
||||
->where('objectID', $mod->fileID)
|
||||
->orderBy('labelID', 'desc')
|
||||
->first();
|
||||
|
||||
if (empty($versionLabel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $mod->fileID,
|
||||
'user_id' => User::whereHubId($mod->userID)->value('id'),
|
||||
'name' => $modContent?->subject ?? '',
|
||||
'slug' => Str::slug($modContent?->subject) ?? '',
|
||||
'teaser' => Str::limit($modContent?->teaser) ?? '',
|
||||
'description' => $this->convertModDescription($modContent?->message ?? ''),
|
||||
'thumbnail' => $this->fetchModThumbnail($command, $curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
|
||||
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
|
||||
'source_code_link' => $optionSourceCode?->source_code_link ?? '',
|
||||
'featured' => (bool) $mod->isFeatured,
|
||||
'contains_ai_content' => (bool) $optionContainsAi?->contains_ai ?? false,
|
||||
'contains_ads' => (bool) $optionContainsAds?->contains_ads ?? false,
|
||||
'disabled' => (bool) $mod->isDisabled,
|
||||
'created_at' => Carbon::parse($mod->time, 'UTC'),
|
||||
'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
Mod::upsert($insertData, ['hub_id'], ['user_id', 'name', 'slug', 'teaser', 'description', 'thumbnail', 'license_id', 'source_code_link', 'featured', 'contains_ai_content', 'disabled', 'created_at', 'updated_at']);
|
||||
$totalInserted += count($insertData);
|
||||
$command->line('Processed '.count($insertData).' mods. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($mods);
|
||||
}, 'fileID');
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
$this->info('Total mods processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function bringFileOptionsLocal(): void
|
||||
{
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
|
||||
fileID INT,
|
||||
optionID INT,
|
||||
optionValue VARCHAR(255)
|
||||
)');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_option_value')
|
||||
->orderBy('fileID')
|
||||
->chunk(200, function ($options) {
|
||||
foreach ($options as $option) {
|
||||
DB::table('temp_file_option_values')->insert([
|
||||
'fileID' => $option->fileID,
|
||||
'optionID' => $option->optionID,
|
||||
'optionValue' => $option->optionValue,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function bringFileContentLocal(): void
|
||||
{
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_content (
|
||||
fileID INT,
|
||||
subject VARCHAR(255),
|
||||
teaser VARCHAR(255),
|
||||
message LONGTEXT
|
||||
)');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_content')
|
||||
->orderBy('fileID')
|
||||
->chunk(200, function ($contents) {
|
||||
foreach ($contents as $content) {
|
||||
DB::table('temp_file_content')->insert([
|
||||
'fileID' => $content->fileID,
|
||||
'subject' => $content->subject,
|
||||
'teaser' => $content->teaser,
|
||||
'message' => $content->message,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fetchModThumbnail($command, $curl, string $fileID, string $thumbnailHash, string $thumbnailExtension): string
|
||||
{
|
||||
if (empty($fileID) || empty($thumbnailHash) || empty($thumbnailExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Only the first two characters of the icon hash.
|
||||
$hashShort = substr($thumbnailHash, 0, 2);
|
||||
|
||||
$hubUrl = "https://hub.sp-tarkov.com/files/images/file/$hashShort/$fileID.$thumbnailExtension";
|
||||
$localPath = "mods/$thumbnailHash.$thumbnailExtension";
|
||||
|
||||
// Check to make sure the image doesn't already exist.
|
||||
if (Storage::disk('public')->exists($localPath)) {
|
||||
return "/storage/$localPath";
|
||||
}
|
||||
|
||||
$command->output->write("Downloading mod thumbnail: $hubUrl... ");
|
||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||
$image = curl_exec($curl);
|
||||
if ($image === false) {
|
||||
$command->error('Error: '.curl_error($curl));
|
||||
} else {
|
||||
Storage::disk('public')->put($localPath, $image);
|
||||
$command->info('Done.');
|
||||
}
|
||||
|
||||
// Return the path to the saved thumbnail.
|
||||
return "/storage/$localPath";
|
||||
}
|
||||
|
||||
protected function importModVersions(): void
|
||||
{
|
||||
$command = $this;
|
||||
$totalInserted = 0;
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_version')
|
||||
->chunkById(500, function (Collection $versions) use (&$command, &$totalInserted) {
|
||||
|
||||
foreach ($versions as $version) {
|
||||
|
||||
$versionContent = DB::table('temp_file_version_content')
|
||||
->select('description')
|
||||
->where('versionID', $version->versionID)
|
||||
->orderBy('versionID', 'desc')
|
||||
->first();
|
||||
|
||||
$optionVirusTotal = DB::table('temp_file_option_values')
|
||||
->select('optionValue as virus_total_link')
|
||||
->where('fileID', $version->fileID)
|
||||
->whereIn('optionID', [6, 2])
|
||||
->whereNot('optionValue', '')
|
||||
->orderByDesc('optionID')
|
||||
->first();
|
||||
|
||||
$versionLabel = DB::table('temp_file_version_labels')
|
||||
->select('labelID')
|
||||
->where('objectID', $version->fileID)
|
||||
->orderBy('labelID', 'desc')
|
||||
->first();
|
||||
|
||||
$modId = Mod::whereHubId($version->fileID)->value('id');
|
||||
|
||||
if (empty($versionLabel) || empty($modId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => $version->versionID,
|
||||
'mod_id' => $modId,
|
||||
'version' => $version->versionNumber,
|
||||
'description' => $this->convertModDescription($versionContent->description ?? ''),
|
||||
'link' => $version->downloadURL,
|
||||
'spt_version_id' => SptVersion::whereHubId($versionLabel->labelID)->value('id'),
|
||||
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
||||
'downloads' => max((int) $version->downloads, 0), // Ensure the value is at least 0
|
||||
'disabled' => (bool) $version->isDisabled,
|
||||
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
ModVersion::upsert($insertData, ['hub_id'], ['mod_id', 'version', 'description', 'link', 'spt_version_id', 'virus_total_link', 'downloads', 'created_at', 'updated_at']);
|
||||
$totalInserted += count($insertData);
|
||||
$command->line('Processed '.count($insertData).' mod versions. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($version);
|
||||
}, 'versionID');
|
||||
|
||||
$this->info('Total mod versions processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function bringFileVersionLabelsLocal(): void
|
||||
{
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
|
||||
labelID INT,
|
||||
objectID INT
|
||||
)');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_label_object')
|
||||
->where('objectTypeID', 387)
|
||||
->orderBy('labelID')
|
||||
->chunk(200, function ($options) {
|
||||
foreach ($options as $option) {
|
||||
DB::table('temp_file_version_labels')->insert([
|
||||
'labelID' => $option->labelID,
|
||||
'objectID' => $option->objectID,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function bringFileVersionContentLocal(): void
|
||||
{
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
|
||||
versionID INT,
|
||||
description TEXT
|
||||
)');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_version_content')
|
||||
->orderBy('versionID')
|
||||
->chunk(200, function ($options) {
|
||||
foreach ($options as $option) {
|
||||
DB::table('temp_file_version_content')->insert([
|
||||
'versionID' => $option->versionID,
|
||||
'description' => $option->description,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function convertModDescription(string $description): string
|
||||
{
|
||||
// Alright, hear me out... Shut up.
|
||||
$converter = new HtmlConverter();
|
||||
|
||||
return $converter->convert(Purify::clean($description));
|
||||
}
|
||||
}
|
@ -1,467 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
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;
|
||||
use Illuminate\Support\Str;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
|
||||
class ImportWoltlabData extends Command
|
||||
{
|
||||
protected $signature = 'app:import-woltlab-data';
|
||||
|
||||
protected $description = 'Connects to the Woltlab database and imports the data into the Laravel database.';
|
||||
|
||||
protected array $fileOptionValues = [];
|
||||
|
||||
protected array $fileContent = [];
|
||||
|
||||
protected array $fileVersionContent = [];
|
||||
|
||||
protected array $fileVersionLabels = [];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->newLine();
|
||||
|
||||
$totalTime = Benchmark::value(function () {
|
||||
$loadDataTime = Benchmark::value(function () {
|
||||
$this->loadData();
|
||||
});
|
||||
$this->info('Execution time: '.round($loadDataTime[1], 2).'ms');
|
||||
$this->newLine();
|
||||
|
||||
$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->fileVersionLabels = $this->getFileVersionLabels();
|
||||
$this->fileVersionContent = $this->getFileVersionContent();
|
||||
$this->info('Done.');
|
||||
}
|
||||
|
||||
protected function importUsers(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
|
||||
DB::connection('mysql_woltlab')->table('wcf1_user')->chunkById(2500, function (Collection $users) use (&$totalInserted) {
|
||||
$insertData = [];
|
||||
foreach ($users as $wolt) {
|
||||
$registrationDate = Carbon::parse($wolt->registrationDate, 'UTC');
|
||||
if ($registrationDate->isFuture()) {
|
||||
$registrationDate = now('UTC');
|
||||
}
|
||||
$registrationDate->setTimezone('UTC');
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => $wolt->userID,
|
||||
'name' => $wolt->username,
|
||||
'email' => mb_convert_case($wolt->email, MB_CASE_LOWER, 'UTF-8'),
|
||||
'password' => $this->cleanPasswordHash($wolt->password),
|
||||
'created_at' => $registrationDate,
|
||||
'updated_at' => now('UTC')->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
User::upsert($insertData, ['hub_id'], ['name', 'email', 'password', 'created_at', 'updated_at']);
|
||||
$totalInserted += count($insertData);
|
||||
$this->line('Processed '.count($insertData).' users. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($users);
|
||||
}, 'userID');
|
||||
|
||||
$this->info('Total users processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function cleanPasswordHash(string $password): string
|
||||
{
|
||||
// The WoltLab password hash sometimes? has a prefix of the hash type. We only want the hash.
|
||||
return str_replace(['Bcrypt:', 'cryptMD5:', 'cryptMD5::'], '', $password);
|
||||
}
|
||||
|
||||
protected function importLicenses(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
|
||||
DB::connection('mysql_woltlab')->table('filebase1_license')->chunkById(100, function (Collection $licenses) use (&$totalInserted) {
|
||||
$insertData = [];
|
||||
foreach ($licenses as $license) {
|
||||
$insertData[] = [
|
||||
'hub_id' => $license->licenseID,
|
||||
'name' => $license->licenseName,
|
||||
'link' => $license->licenseURL,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
DB::table('licenses')->upsert($insertData, ['hub_id'], ['name', 'link']);
|
||||
$totalInserted += count($insertData);
|
||||
$this->line('Processed '.count($insertData).' licenses. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($licenses);
|
||||
}, 'licenseID');
|
||||
|
||||
$this->info('Total licenses processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function importSptVersions(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
|
||||
DB::connection('mysql_woltlab')->table('wcf1_label')->where('groupID', 1)->chunkById(100, function (Collection $versions) use (&$totalInserted) {
|
||||
$insertData = [];
|
||||
foreach ($versions as $version) {
|
||||
$insertData[] = [
|
||||
'hub_id' => $version->labelID,
|
||||
'version' => $version->label,
|
||||
'color_class' => $this->translateColour($version->cssClassName),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
DB::table('spt_versions')->upsert($insertData, ['hub_id'], ['version', 'color_class']);
|
||||
$totalInserted += count($insertData);
|
||||
$this->line('Processed '.count($insertData).' SPT Versions. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($versions);
|
||||
}, 'labelID');
|
||||
|
||||
$this->info('Total licenses processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function translateColour(string $colour = ''): string
|
||||
{
|
||||
return match ($colour) {
|
||||
'green' => 'green',
|
||||
'slightly-outdated' => 'lime',
|
||||
'yellow' => 'yellow',
|
||||
'red' => 'red',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
protected function importMods(): void
|
||||
{
|
||||
$command = $this;
|
||||
$totalInserted = 0;
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
|
||||
DB::connection('mysql_woltlab')->table('filebase1_file')->chunkById(100, function (Collection $mods) use (&$command, &$curl, &$totalInserted) {
|
||||
|
||||
foreach ($mods as $mod) {
|
||||
$modContent = $this->fileContent[$mod->fileID] ?? [];
|
||||
$modOptions = $this->fileOptionValues[$mod->fileID] ?? [];
|
||||
$versionLabel = $this->fileVersionLabels[$mod->fileID] ?? [];
|
||||
|
||||
if (empty($versionLabel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $mod->fileID,
|
||||
'user_id' => User::whereHubId($mod->userID)->value('id'),
|
||||
'name' => $modContent ? $modContent->subject : '',
|
||||
'slug' => $modContent ? Str::slug($modContent->subject) : '',
|
||||
'teaser' => $modContent ? (strlen($modContent->teaser) > 100 ? Str::take($modContent->teaser, 97).'...' : $modContent->teaser) : '',
|
||||
'description' => $this->convertModDescription($modContent?->message ?? ''),
|
||||
'thumbnail' => $this->fetchModThumbnail($command, $curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
|
||||
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
|
||||
'source_code_link' => $this->fetchSourceLinkValue($modOptions),
|
||||
'featured' => (bool) $mod->isFeatured,
|
||||
'contains_ai_content' => $this->fetchContainsAiContentValue($modOptions),
|
||||
'contains_ads' => $this->fetchContainsAdsValue($modOptions),
|
||||
'disabled' => (bool) $mod->isDisabled,
|
||||
'created_at' => Carbon::parse($mod->time, 'UTC'),
|
||||
'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
Mod::upsert($insertData, ['hub_id'], ['user_id', 'name', 'slug', 'teaser', 'description', 'thumbnail', 'license_id', 'source_code_link', 'featured', 'contains_ai_content', 'disabled', 'created_at', 'updated_at']);
|
||||
$totalInserted += count($insertData);
|
||||
$command->line('Processed '.count($insertData).' mods. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($mods);
|
||||
}, 'fileID');
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
$this->info('Total mods processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function getFileOptionValues(): array
|
||||
{
|
||||
// Fetch all the data from the `filebase1_file_option_value` table.
|
||||
$options = DB::connection('mysql_woltlab')->table('filebase1_file_option_value')->get();
|
||||
|
||||
// Convert the collection into an associative array
|
||||
$optionValues = [];
|
||||
foreach ($options as $option) {
|
||||
$optionValues[$option->fileID][] = $option;
|
||||
}
|
||||
|
||||
return $optionValues;
|
||||
}
|
||||
|
||||
protected function getFileContent(): array
|
||||
{
|
||||
$content = [];
|
||||
|
||||
// Fetch select data from the `filebase1_file_content` table.
|
||||
DB::connection('mysql_woltlab')
|
||||
->table('filebase1_file_content')
|
||||
->select(['fileID', 'subject', 'teaser', 'message'])
|
||||
->orderBy('fileID', 'desc')
|
||||
->chunk(200, function ($contents) use (&$content) {
|
||||
foreach ($contents as $contentItem) {
|
||||
$content[$contentItem->fileID] = $contentItem;
|
||||
}
|
||||
});
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fetchSourceLinkValue(array $options): string
|
||||
{
|
||||
// Iterate over the options and find the 'optionID' of 5 or 1. Those records will contain the source code link
|
||||
// in the 'optionValue' column. The 'optionID' of 5 should take precedence over 1. If neither are found, return
|
||||
// an empty string.
|
||||
foreach ($options as $option) {
|
||||
if ($option->optionID == 5 && ! empty($option->optionValue)) {
|
||||
return $option->optionValue;
|
||||
}
|
||||
if ($option->optionID == 1 && ! empty($option->optionValue)) {
|
||||
return $option->optionValue;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function fetchContainsAiContentValue(array $options): bool
|
||||
{
|
||||
// Iterate over the options and find the 'optionID' of 7. That record will contain the AI flag.
|
||||
foreach ($options as $option) {
|
||||
if ($option->optionID == 7) {
|
||||
return (bool) $option->optionValue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function fetchContainsAdsValue(array $options): bool
|
||||
{
|
||||
// Iterate over the options and find the 'optionID' of 3. That record will contain the Ad flag.
|
||||
foreach ($options as $option) {
|
||||
if ($option->optionID == 3) {
|
||||
return (bool) $option->optionValue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function fetchModThumbnail($command, &$curl, string $fileID, string $thumbnailHash, string $thumbnailExtension): string
|
||||
{
|
||||
if (empty($fileID) || empty($thumbnailHash) || empty($thumbnailExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Only the first two characters of the icon hash.
|
||||
$hashShort = substr($thumbnailHash, 0, 2);
|
||||
|
||||
$hubUrl = "https://hub.sp-tarkov.com/files/images/file/$hashShort/$fileID.$thumbnailExtension";
|
||||
$localPath = "mods/$thumbnailHash.$thumbnailExtension";
|
||||
|
||||
// Check to make sure the image doesn't already exist.
|
||||
if (Storage::disk('public')->exists($localPath)) {
|
||||
return "/storage/$localPath";
|
||||
}
|
||||
|
||||
$command->output->write("Downloading mod thumbnail: $hubUrl... ");
|
||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||
$image = curl_exec($curl);
|
||||
if ($image === false) {
|
||||
$command->error('Error: '.curl_error($curl));
|
||||
} else {
|
||||
Storage::disk('public')->put($localPath, $image);
|
||||
$command->info('Done.');
|
||||
}
|
||||
|
||||
// Return the path to the saved thumbnail.
|
||||
return "/storage/$localPath";
|
||||
}
|
||||
|
||||
protected function getFileVersionContent(): array
|
||||
{
|
||||
$content = [];
|
||||
|
||||
// Fetch select data from the `filebase1_file_version_content` table.
|
||||
DB::connection('mysql_woltlab')
|
||||
->table('filebase1_file_version_content')
|
||||
->select(['versionID', 'description'])
|
||||
->orderBy('versionID', 'desc')
|
||||
->chunk(100, function ($contents) use (&$content) {
|
||||
foreach ($contents as $contentItem) {
|
||||
$content[$contentItem->versionID] = $content;
|
||||
}
|
||||
});
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function getFileVersionLabels(): array
|
||||
{
|
||||
$labels = [];
|
||||
|
||||
// Fetch select data from the `wcf1_label_object` table.
|
||||
DB::connection('mysql_woltlab')
|
||||
->table('wcf1_label_object')
|
||||
->select(['labelID', 'objectID'])
|
||||
->where('objectTypeID', 387)
|
||||
->orderBy('labelID', 'desc')
|
||||
->chunk(100, function ($labelData) use (&$labels) {
|
||||
foreach ($labelData as $labelItem) {
|
||||
$labels[$labelItem->objectID] = $labelItem->labelID;
|
||||
}
|
||||
});
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
protected function importModVersions(): void
|
||||
{
|
||||
$command = $this;
|
||||
$totalInserted = 0;
|
||||
|
||||
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] ?? [];
|
||||
$modOptions = $this->fileOptionValues[$version->fileID] ?? [];
|
||||
$versionLabel = $this->fileVersionLabels[$version->fileID] ?? [];
|
||||
|
||||
$modId = Mod::whereHubId($version->fileID)->value('id');
|
||||
|
||||
if (empty($versionLabel) || empty($modId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => $version->versionID,
|
||||
'mod_id' => $modId,
|
||||
'version' => $version->versionNumber,
|
||||
'description' => $this->convertModDescription($versionContent['description'] ?? ''),
|
||||
'link' => $version->downloadURL,
|
||||
'spt_version_id' => SptVersion::whereHubId($versionLabel)->value('id'),
|
||||
'virus_total_link' => $this->fetchVirusTotalLink($modOptions),
|
||||
'downloads' => max((int) $version->downloads, 0), // Ensure the value is at least 0
|
||||
'disabled' => (bool) $version->isDisabled,
|
||||
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
ModVersion::upsert($insertData, ['hub_id'], ['mod_id', 'version', 'description', 'link', 'spt_version_id', 'virus_total_link', 'downloads', 'created_at', 'updated_at']);
|
||||
$totalInserted += count($insertData);
|
||||
$command->line('Processed '.count($insertData).' mod versions. Total processed so far: '.$totalInserted);
|
||||
}
|
||||
|
||||
unset($insertData);
|
||||
unset($version);
|
||||
}, 'versionID');
|
||||
|
||||
$this->info('Total mod versions processed: '.$totalInserted);
|
||||
}
|
||||
|
||||
protected function fetchVirusTotalLink(array $options): string
|
||||
{
|
||||
// Iterate over the options and find the 'optionID' of 6 or 2. Those records will contain the Virus Total link
|
||||
// in the 'optionValue' column. The 'optionID' of 6 should take precedence over 1. If neither are found, return
|
||||
// an empty string.
|
||||
foreach ($options as $option) {
|
||||
if ($option->optionID == 6 && ! empty($option->optionValue)) {
|
||||
return $option->optionValue;
|
||||
}
|
||||
if ($option->optionID == 2 && ! empty($option->optionValue)) {
|
||||
return $option->optionValue;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function convertModDescription(string $description): string
|
||||
{
|
||||
// Alright, hear me out... Shut up.
|
||||
$converter = new HtmlConverter();
|
||||
|
||||
return $converter->convert(Purify::clean($description));
|
||||
}
|
||||
}
|
@ -12,8 +12,8 @@ class GlobalSearch extends Component
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$results = $this->query ? Mod::search($this->query)->get() : [];
|
||||
|
||||
$results = $this->query ? Mod::search($this->query)->get() : collect();
|
||||
|
||||
return view('livewire.global-search', [
|
||||
'results' => $results,
|
||||
]);
|
||||
|
@ -23,6 +23,7 @@ class Mod extends Model
|
||||
'user_id',
|
||||
'name',
|
||||
'slug',
|
||||
'teaser',
|
||||
'description',
|
||||
'license_id',
|
||||
'source_code_link',
|
||||
|
@ -59,6 +59,26 @@ return [
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mysql_hub' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_HUB_URL'),
|
||||
'host' => env('DB_HUB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_HUB_PORT', '3306'),
|
||||
'database' => env('DB_HUB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_HUB_USERNAME', 'root'),
|
||||
'password' => env('DB_HUB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_HUB_SOCKET', ''),
|
||||
'charset' => env('DB_HUB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_HUB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
@ -108,27 +128,6 @@ return [
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
'mysql_woltlab' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_WOLTLAB_URL'),
|
||||
'host' => env('DB_WOLTLAB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_WOLTLAB_PORT', '3306'),
|
||||
'database' => env('DB_WOLTLAB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_WOLTLAB_USERNAME', 'root'),
|
||||
'password' => env('DB_WOLTLAB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_WOLTLAB_SOCKET', ''),
|
||||
'charset' => env('DB_WOLTLAB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_WOLTLAB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@ -185,6 +184,15 @@ return [
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
|
||||
'queue' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '2'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -4,4 +4,4 @@ includes:
|
||||
parameters:
|
||||
paths:
|
||||
- app/
|
||||
level: 5
|
||||
level: 4
|
||||
|
@ -11,6 +11,11 @@ button[type="button"]:not([role="menuitem"]) {
|
||||
@apply border border-transparent rounded-md py-2 px-4 text-white dark:text-gray-100 bg-gray-900 dark:bg-gray-700 hover:bg-gray-800 dark:hover:bg-black dark:hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-600 dark:focus:ring-gray-500 transition-all duration-200
|
||||
}
|
||||
|
||||
nav button[type="submit"]:not([role="menuitem"]),
|
||||
nav button[type="button"]:not([role="menuitem"]) {
|
||||
@apply border border-transparent rounded-md py-2 px-4 text-white dark:text-gray-100 bg-gray-900 dark:bg-gray-700 hover:bg-gray-800 dark:hover:bg-gray-900 dark:hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-600 dark:focus:ring-gray-500 transition-all duration-200
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@apply text-gray-800 dark:text-gray-300 focus:ring-gray-600 dark:focus:ring-gray-500 border-gray-300 dark:border-gray-700 rounded;
|
||||
}
|
||||
@ -47,39 +52,51 @@ main a:not(.mod-list-component):not(.tab) {
|
||||
b, strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
i, em {
|
||||
@apply italic
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-bold mt-4 mb-2 text-black dark:text-white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply my-2 text-gray-800 dark:text-gray-300;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-disc mb-2;
|
||||
}
|
||||
|
||||
ol {
|
||||
@apply list-decimal mb-2;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply my-2 ml-7 text-gray-800 dark:text-gray-300;
|
||||
}
|
||||
|
@ -1,5 +1 @@
|
||||
<svg viewBox="0 0 317 48" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M74.09 30.04V13h-4.14v21H82.1v-3.96h-8.01zM95.379 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM106.628 21.58V19h-3.87v15h3.87v-7.17c0-3.15 2.55-4.05 4.56-3.81V18.7c-1.89 0-3.78.84-4.56 2.88zM124.295 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM141.544 19l-3.66 10.5-3.63-10.5h-4.26l5.7 15h4.41l5.7-15h-4.26zM150.354 28.09h11.31c.09-.51.15-1.02.15-1.59 0-4.41-3.15-7.92-7.59-7.92-4.71 0-7.92 3.45-7.92 7.92s3.18 7.92 8.22 7.92c2.88 0 5.13-1.17 6.54-3.21l-3.12-1.8c-.66.87-1.86 1.5-3.36 1.5-2.04 0-3.69-.84-4.23-2.82zm-.06-3c.45-1.92 1.86-3.03 3.93-3.03 1.62 0 3.24.87 3.72 3.03h-7.65zM164.516 34h3.87V12.1h-3.87V34zM185.248 34.36c3.69 0 6.9-2.01 6.9-6.3V13h-2.1v15.06c0 3.03-2.07 4.26-4.8 4.26-2.19 0-3.93-.78-4.62-2.61l-1.77 1.05c1.05 2.43 3.57 3.6 6.39 3.6zM203.124 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM221.224 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM225.176 22.93c0-1.62 1.59-2.37 3.15-2.37 1.44 0 2.97.57 3.6 2.1l1.65-.96c-.87-1.86-2.79-3.06-5.25-3.06-3 0-5.13 1.89-5.13 4.29 0 5.52 8.76 3.39 8.76 7.11 0 1.77-1.68 2.4-3.45 2.4-2.01 0-3.57-.99-4.11-2.52l-1.68.99c.75 1.92 2.79 3.45 5.79 3.45 3.21 0 5.43-1.77 5.43-4.32 0-5.52-8.76-3.39-8.76-7.11zM244.603 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM249.883 21.49V19h-1.98v15h1.98v-8.34c0-3.72 2.34-4.98 4.74-4.98v-1.92c-1.92 0-3.69.63-4.74 2.73zM263.358 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM286.848 19v2.94c-1.26-2.01-3.39-3.3-6.06-3.3-4.23 0-7.74 3.42-7.74 7.86s3.51 7.86 7.74 7.86c2.67 0 4.8-1.29 6.06-3.3V34h1.98V19h-1.98zm-5.91 13.44c-3.33 0-5.91-2.61-5.91-5.94 0-3.33 2.58-5.94 5.91-5.94s5.91 2.61 5.91 5.94c0 3.33-2.58 5.94-5.91 5.94zM309.01 18.64c-1.92 0-3.75.87-4.86 2.73-.84-1.74-2.46-2.73-4.56-2.73-1.8 0-3.42.72-4.59 2.55V19h-1.98v15H295v-8.31c0-3.72 2.16-5.13 4.32-5.13 2.13 0 3.51 1.41 3.51 4.08V34h1.98v-8.31c0-3.72 1.86-5.13 4.17-5.13 2.13 0 3.66 1.41 3.66 4.08V34h1.98v-9.36c0-3.75-2.31-6-5.61-6z" class="fill-black"/>
|
||||
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5"/>
|
||||
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5"/>
|
||||
</svg>
|
||||
{{-- Logo --}}
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 15 B |
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 dark:bg-gray-800 focus:outline-none focus:text-indigo-800 dark:focus:text-gray-300 focus:bg-indigo-100 dark:focus:bg-gray-700 focus:border-indigo-700 dark:focus:border-gray-700 transition duration-150 ease-in-out'
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-grey-400 text-start text-base font-medium text-gray-700 bg-gray-50 dark:bg-gray-800 focus:outline-none focus:text-gray-800 dark:focus:text-gray-300 focus:bg-gray-100 dark:focus:bg-gray-700 focus:border-gray-700 dark:focus:border-gray-700 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-800 dark:focus:text-gray-300 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
|
@ -1,96 +1,5 @@
|
||||
<div class="p-6 lg:p-8 bg-white border-b border-gray-200">
|
||||
<x-application-logo class="block h-12 w-auto" />
|
||||
|
||||
<h1 class="mt-8 text-2xl font-medium text-gray-900">
|
||||
Welcome to your Jetstream application!
|
||||
</h1>
|
||||
|
||||
<p class="mt-6 text-gray-500 leading-relaxed">
|
||||
Laravel Jetstream provides a beautiful, robust starting point for your next Laravel application. Laravel is designed
|
||||
to help you build your application using a development environment that is simple, powerful, and enjoyable. We believe
|
||||
you should love expressing your creativity through programming, so we have spent time carefully crafting the Laravel
|
||||
ecosystem to be a breath of fresh air. We hope you love it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-200 bg-opacity-25 grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8 p-6 lg:p-8">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
<a href="https://laravel.com/docs">Documentation</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Laravel has wonderful documentation covering every aspect of the framework. Whether you're new to the framework or have previous experience, we recommend reading all of the documentation from beginning to end.
|
||||
</p>
|
||||
|
||||
<p class="mt-4 text-sm">
|
||||
<a href="https://laravel.com/docs" class="inline-flex items-center font-semibold text-indigo-700">
|
||||
Explore the documentation
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 w-5 h-5 fill-indigo-500">
|
||||
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
<a href="https://laracasts.com">Laracasts</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
|
||||
</p>
|
||||
|
||||
<p class="mt-4 text-sm">
|
||||
<a href="https://laracasts.com" class="inline-flex items-center font-semibold text-indigo-700">
|
||||
Start watching Laracasts
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 w-5 h-5 fill-indigo-500">
|
||||
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
<a href="https://tailwindcss.com/">Tailwind</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Laravel Jetstream is built with Tailwind, an amazing utility first CSS framework that doesn't get in your way. You'll be amazed how easily you can build and maintain fresh, modern designs with this wonderful framework at your fingertips.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
Authentication
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Authentication and registration views are included with Laravel Jetstream, as well as support for user email verification and resetting forgotten passwords. So, you're free to get started with what matters most: building your application.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 lg:p-8 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
|
||||
<x-application-logo class="block h-12 w-auto"/>
|
||||
<h1 class="text-2xl font-medium text-gray-900 dark:text-white">Welcome {{ auth()->user()->name }}!</h1>
|
||||
<p class="mt-6 text-gray-500 dark:text-gray-200 leading-relaxed">I don't know why you're here. There's not much to see yet.</p>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<x-app-layout>
|
||||
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-900 overflow-hidden shadow-xl dark:shadow-gray-900 sm:rounded-lg">
|
||||
<div class="relative isolate overflow-hidden bg-gray-900 dark:bg-gray-800 px-6 py-24 sm:py-32 lg:px-8 rounded-md">
|
||||
<div class="bg-white dark:bg-gray-900 overflow-hidden shadow-xl dark:shadow-gray-900 rounded-none sm:rounded-lg">
|
||||
<div class="relative isolate overflow-hidden bg-gray-900 dark:bg-gray-800 px-6 py-24 sm:py-32 lg:px-8 rounded-none sm:rounded-md">
|
||||
<video autoplay muted loop class="absolute inset-0 -z-10 h-full w-full object-cover">
|
||||
<source src="/video/welcome.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
@ -44,7 +44,7 @@
|
||||
</header>
|
||||
@endif
|
||||
|
||||
<main class="py-6 sm:py-12">
|
||||
<main class="pb-6 sm:py-12">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
|
@ -1,65 +1,37 @@
|
||||
<div class="relative z-10" role="dialog" aria-modal="true">
|
||||
<div
|
||||
x-cloak
|
||||
x-show="searchOpen"
|
||||
x-transition:enter="transition ease-out duration-300 transform"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200 transform"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-50 transition-opacity"
|
||||
></div>
|
||||
|
||||
<div
|
||||
x-cloak
|
||||
x-show="searchOpen"
|
||||
x-transition:enter="transition ease-out duration-300 transform"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200 transform"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@keyup.escape.window="searchOpen = false"
|
||||
class="fixed inset-0 z-10 w-screen overflow-y-auto p-4 sm:p-6 md:p-20"
|
||||
>
|
||||
<div
|
||||
@click.outside="searchOpen = false"
|
||||
class="mx-auto max-w-2xl transform divide-y divide-gray-100 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all"
|
||||
>
|
||||
<div x-cloak x-show="searchOpen" x-transition:enter="transition ease-out duration-300 transform" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200 transform" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 bg-gray-900 dark:bg-gray-400 bg-opacity-80 dark:bg-opacity-80 transition-opacity"></div>
|
||||
<div x-cloak x-show="searchOpen" x-transition:enter="transition ease-out duration-300 transform" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200 transform" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @keyup.escape.window="searchOpen = false" class="fixed inset-0 z-10 w-screen overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<div @click.outside="searchOpen = false" class="mx-auto max-w-2xl transform divide-y divide-gray-100 dark:divide-gray-500 overflow-hidden rounded-xl bg-white dark:bg-gray-900 shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<div class="relative">
|
||||
<svg class="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<input wire:model.live="query" id="global-search" type="text" class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm" placeholder="{{ __('Search for a mod...') }}">
|
||||
<input wire:model.live="query" id="global-search" type="text" class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 dark:text-white placeholder:text-gray-400 focus:ring-0 sm:text-sm" placeholder="{{ __('Search for a mod...') }}">
|
||||
</div>
|
||||
|
||||
<!-- Default state, show/hide based on command palette state. -->
|
||||
@if($results)
|
||||
<ul class="max-h-80 scroll-py-2 divide-y divide-gray-100 overflow-y-auto">
|
||||
@if($results->count() && $this->query)
|
||||
<ul class="max-h-80 scroll-py-2 divide-y divide-gray-100 dark:divide-gray-500 overflow-y-auto">
|
||||
<h2 class="sr-only">{{ __('Search Results') }}</h2>
|
||||
@foreach($results as $result)
|
||||
<li class="p-2">
|
||||
<h2 class="sr-only">{{ __('Search Results') }}</h2>
|
||||
<ul class="text-sm text-gray-700">
|
||||
<!-- Active: "bg-indigo-600 text-white" -->
|
||||
<li class="group flex cursor-default select-none items-center rounded-md px-3 py-2">
|
||||
<!-- Active: "text-white", Not Active: "text-gray-400" -->
|
||||
<li class="text-sm group">
|
||||
<a href="/mod/{{ $result->id }}/{{ $result->slug }}" class="block w-full group flex select-none items-center rounded-md p-3 text-gray-700 hover:text-black focus:text-black dark:text-gray-400 dark:hover:text-white">
|
||||
@if(empty($result->thumbnail))
|
||||
<img src="https://placehold.co/450x450/EEE/31343C?font=source-sans-pro&text={{ $result->name }}" alt="{{ $result->name }}" class="block dark:hidden h-6 w-6 flex-none">
|
||||
<img src="https://placehold.co/450x450/31343C/EEE?font=source-sans-pro&text={{ $result->name }}" alt="{{ $result->name }}" class="hidden dark:block h-6 w-6 flex-none">
|
||||
@else
|
||||
<img src="{{ $result->thumbnail }}" alt="{{ $result->name }}" class="h-6 w-6 flex-none">
|
||||
<span class="ml-3 flex-auto truncate"><a href="/mod/{{ $result->id }}/{{ $result->slug }}">{{ $result->name }}</a></span>
|
||||
<!-- Active: "text-indigo-100", Not Active: "text-gray-400" -->
|
||||
<span class="ml-3 flex-none text-xs font-semibold text-gray-400">Mod</span>
|
||||
</li>
|
||||
</ul>
|
||||
@endif
|
||||
<span class="ml-3 flex-auto truncate">{{ $result->name }}</span>
|
||||
<span class="ml-3 flex-none text-xs font-semibold text-gray-400">Mod</span> </a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<!-- Empty state, show/hide based on command palette state. -->
|
||||
@elseif(!$results->count() && $this->query)
|
||||
<div class="px-6 py-14 text-center sm:px-14">
|
||||
<svg class="mx-auto h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
|
||||
<svg class="mx-auto h-6 w-6 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/>
|
||||
</svg>
|
||||
<p class="mt-4 text-sm text-gray-900">{{ __('We couldn\'t find any projects with that term. Please try again.') }}</p>
|
||||
<p class="mt-4 text-sm text-gray-900 dark:text-gray-200">{{ __("We couldn't find any content with that query. Please try again.") }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
@ -35,8 +35,8 @@
|
||||
<div>
|
||||
<div class="sm:hidden">
|
||||
<label for="tabs" class="sr-only">Select a tab</label>
|
||||
<!-- Use an "onChange" listener to redirect the user to the selected tab URL. -->
|
||||
<select id="tabs" name="tabs" class="block w-full rounded-md border-gray-300 focus:border-grey-500 focus:ring-grey-500">
|
||||
{{-- Use an "onChange" listener to redirect the user to the selected tab URL. --}}
|
||||
<select id="tabs" name="tabs" class="block w-full rounded-md dark:text-white bg-gray-100 dark:bg-gray-950 border-gray-300 dark:border-gray-700 focus:border-grey-500 dark:focus:border-grey-600 focus:ring-grey-500 dark:focus:ring-grey-600">
|
||||
<option selected>Description</option>
|
||||
<option>Versions</option>
|
||||
<option>Comments</option>
|
||||
@ -46,8 +46,7 @@
|
||||
<nav class="isolate flex divide-x divide-gray-200 dark:divide-gray-800 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl" aria-label="Tabs">
|
||||
<a href="#description" class="tab rounded-l-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10" aria-current="page">
|
||||
<span>Description</span>
|
||||
<span aria-hidden="true" class="bg-gray-500 absolute inset-x-0 bottom-0 h-0.5"></span>
|
||||
</a>
|
||||
<span aria-hidden="true" class="bg-gray-500 absolute inset-x-0 bottom-0 h-0.5"></span> </a>
|
||||
<a href="#versions" class="tab group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
||||
<span>Versions</span>
|
||||
<span aria-hidden="true" class="bg-transparent absolute inset-x-0 bottom-0 h-0.5"></span>
|
||||
@ -105,13 +104,23 @@
|
||||
</li>
|
||||
@endif
|
||||
@if($mod->contains_ads)
|
||||
<li class="px-4 py-4 sm:px-0">
|
||||
<h3>{{ __('Includes Advertising') }}</h3>
|
||||
<li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
|
||||
<svg class="grow-0 w-[16px] h-[16px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<h3 class="grow">
|
||||
{{ __('Includes Advertising') }}
|
||||
</h3>
|
||||
</li>
|
||||
@endif
|
||||
@if($mod->contains_ai_content)
|
||||
<li class="px-4 py-4 sm:px-0">
|
||||
<h3>{{ __('Includes AI Generated Content') }}</h3>
|
||||
<li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
|
||||
<svg class="grow-0 w-[16px] h-[16px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<h3 class="grow">
|
||||
{{ __('Includes AI Generated Content') }}
|
||||
</h3>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
|
@ -12,16 +12,14 @@
|
||||
|
||||
{{-- Navigation Links --}}
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">
|
||||
{{ __('Mods') }}
|
||||
</x-nav-link>
|
||||
{{-- <x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">{{ __('Mods') }}</x-nav-link> --}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6 gap-4">
|
||||
<button type="button" @click="searchOpen = !searchOpen; $nextTick(() => { document.querySelector('#global-search').focus(); });" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@ -35,21 +33,21 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@guest()
|
||||
@guest
|
||||
<div class="ml-4">
|
||||
<a href="{{ route('login') }}" class="text-sm font-semibold leading-6 text-gray-900">Log in <span aria-hidden="true">→</span></a>
|
||||
<a href="{{ route('login') }}" class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100">Log in
|
||||
<span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
@endguest
|
||||
|
||||
{{-- Settings Dropdown --}}
|
||||
<div class="ms-3 relative">
|
||||
<div class="relative">
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
@auth()
|
||||
@auth
|
||||
<span class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-500 focus:outline-none focus:bg-gray-50 dark:focus:bg-gray-700 active:bg-gray-50 dark:active:bg-gray-700 transition ease-in-out duration-150">
|
||||
{{ Auth::user()->name }}
|
||||
|
||||
{{ auth()->user()->name }}
|
||||
<svg class="ms-2 -me-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/>
|
||||
</svg>
|
||||
@ -76,10 +74,8 @@
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
|
||||
{{-- Authentication --}}
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link href="{{ route('logout') }}" @click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
@ -104,48 +100,53 @@
|
||||
{{-- Responsive Navigation Menu --}}
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
<x-responsive-nav-link href="{{ route('home') }}" :active="request()->routeIs('home')">
|
||||
{{ __('Home') }}
|
||||
</x-responsive-nav-link>
|
||||
@auth()
|
||||
<x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
{{-- Responsive Settings Options --}}
|
||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center px-4">
|
||||
@auth()
|
||||
@auth
|
||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||
<div class="shrink-0 me-3">
|
||||
<img class="h-10 w-10 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}"/>
|
||||
<img class="h-10 w-10 rounded-full object-cover" src="{{ auth()->user()->profile_photo_url }}" alt="{{ auth()->user()->name }}"/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-base text-gray-800 dark:text-gray-300">{{ Auth::user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500 dark:text-gray-300">{{ Auth::user()->email }}</div>
|
||||
<div class="font-medium text-base text-gray-800 dark:text-gray-300">{{ auth()->user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500 dark:text-gray-300">{{ auth()->user()->email }}</div>
|
||||
</div>
|
||||
@endauth()
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
{{-- Account Management --}}
|
||||
<x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<x-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
|
||||
{{ __('API Tokens') }}
|
||||
@auth
|
||||
{{-- Account Management --}}
|
||||
<x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
@endif
|
||||
|
||||
{{-- Authentication --}}
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<x-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
|
||||
{{ __('API Tokens') }}
|
||||
</x-responsive-nav-link>
|
||||
@endif
|
||||
|
||||
<x-responsive-nav-link href="{{ route('logout') }}" @click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
{{-- Authentication --}}
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link href="{{ route('logout') }}" @click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use App\Console\Commands\ImportHub;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote')->hourly();
|
||||
Schedule::command(ImportHub::class)->hourly();
|
||||
|
Loading…
x
Reference in New Issue
Block a user