Updates to search

This commit is contained in:
Refringe 2024-06-05 16:31:29 -04:00
parent 054c458b0f
commit 92b04eb286
Signed by: Refringe
GPG Key ID: 7715B85B4A6306ED
18 changed files with 654 additions and 737 deletions

View File

@ -3,14 +3,12 @@ APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://localhost APP_URL=http://forge.test
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file VITE_APP_NAME="${APP_NAME}"
APP_MAINTENANCE_STORE=database
# Much higher in production. # Much higher in production.
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
@ -21,23 +19,25 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=laravel DB_DATABASE=forge
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD= 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: # For normal development you should just seed the database with fake data:
# `php artisan migrate:fresh --seed` # `php artisan migrate:fresh --seed`
DB_WOLTLAB_CONNECTION=mysql DB_HUB_CONNECTION=mysql
DB_WOLTLAB_HOST=127.0.0.1 DB_HUB_HOST=localhost
DB_WOLTLAB_PORT=3306 DB_HUB_PORT=3306
DB_WOLTLAB_DATABASE=laravel DB_HUB_DATABASE=forge
DB_WOLTLAB_USERNAME=root DB_HUB_USERNAME=root
DB_WOLTLAB_PASSWORD= DB_HUB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=redis
SESSION_STORE=redis
SESSION_CONNECTION=default
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_PATH=/ SESSION_PATH=/
@ -45,36 +45,30 @@ SESSION_DOMAIN=null
BROADCAST_CONNECTION=log BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database CACHE_STORE=redis
CACHE_PREFIX= QUEUE_CONNECTION=redis
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_CACHE_CONNECTION=cache
REDIS_QUEUE_CONNECTION=queue
REDIS_QUEUE=queue
SCOUT_DRIVER=meilisearch SCOUT_DRIVER=meilisearch
SCOUT_QUEUE=true
MEILISEARCH_HOST=http://127.0.0.1:7700 MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=FORGE MEILISEARCH_KEY=LARAVEL-HERD
MEILISEARCH_NO_ANALYTICS=true MEILISEARCH_NO_ANALYTICS=true
MAIL_MAILER=log MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=${APP_NAME}
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID= NOVA_LICENSE_KEY=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

View 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));
}
}

View File

@ -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));
}
}

View File

@ -12,7 +12,7 @@ class GlobalSearch extends Component
public function render(): View 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', [ return view('livewire.global-search', [
'results' => $results, 'results' => $results,

View File

@ -23,6 +23,7 @@ class Mod extends Model
'user_id', 'user_id',
'name', 'name',
'slug', 'slug',
'teaser',
'description', 'description',
'license_id', 'license_id',
'source_code_link', 'source_code_link',

View File

@ -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' => [ 'mariadb' => [
'driver' => 'mariadb', 'driver' => 'mariadb',
'url' => env('DB_URL'), 'url' => env('DB_URL'),
@ -108,27 +128,6 @@ return [
// 'encrypt' => env('DB_ENCRYPT', 'yes'), // 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), // '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'), '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'),
],
], ],
]; ];

View File

@ -4,4 +4,4 @@ includes:
parameters: parameters:
paths: paths:
- app/ - app/
level: 5 level: 4

View File

@ -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 @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"] { 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; @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 { b, strong {
@apply font-bold; @apply font-bold;
} }
i, em { i, em {
@apply italic @apply italic
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
@apply font-bold mt-4 mb-2 text-black dark:text-white; @apply font-bold mt-4 mb-2 text-black dark:text-white;
} }
h1 { h1 {
@apply text-2xl; @apply text-2xl;
} }
h2 { h2 {
@apply text-xl; @apply text-xl;
} }
h3 { h3 {
@apply text-lg; @apply text-lg;
} }
h4 { h4 {
@apply text-base; @apply text-base;
} }
h5 { h5 {
@apply text-sm; @apply text-sm;
} }
h6 { h6 {
@apply text-xs; @apply text-xs;
} }
p { p {
@apply my-2 text-gray-800 dark:text-gray-300; @apply my-2 text-gray-800 dark:text-gray-300;
} }
ul { ul {
@apply list-disc mb-2; @apply list-disc mb-2;
} }
ol { ol {
@apply list-decimal mb-2; @apply list-decimal mb-2;
} }
li { li {
@apply my-2 ml-7 text-gray-800 dark:text-gray-300; @apply my-2 ml-7 text-gray-800 dark:text-gray-300;
} }

View File

@ -1,5 +1 @@
<svg viewBox="0 0 317 48" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}> {{-- Logo --}}
<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>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 15 B

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
@php @php
$classes = ($active ?? false) $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'; : '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 @endphp

View File

@ -1,96 +1,5 @@
<div class="p-6 lg:p-8 bg-white border-b border-gray-200"> <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" /> <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>
<h1 class="mt-8 text-2xl font-medium text-gray-900"> <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>
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> </div>

View File

@ -1,8 +1,8 @@
<x-app-layout> <x-app-layout>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <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="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-md"> <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"> <video autoplay muted loop class="absolute inset-0 -z-10 h-full w-full object-cover">
<source src="/video/welcome.mp4" type="video/mp4"> <source src="/video/welcome.mp4" type="video/mp4">
</video> </video>

View File

@ -44,7 +44,7 @@
</header> </header>
@endif @endif
<main class="py-6 sm:py-12"> <main class="pb-6 sm:py-12">
{{ $slot }} {{ $slot }}
</main> </main>
</div> </div>

View File

@ -1,65 +1,37 @@
<div class="relative z-10" role="dialog" aria-modal="true"> <div class="relative z-10" role="dialog" aria-modal="true">
<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" class="fixed inset-0 bg-gray-900 dark:bg-gray-400 bg-opacity-80 dark:bg-opacity-80 transition-opacity"></div>
x-cloak <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">
x-show="searchOpen" <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">
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 class="relative"> <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"> <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> </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> </div>
<!-- Default state, show/hide based on command palette state. --> @if($results->count() && $this->query)
@if($results) <ul class="max-h-80 scroll-py-2 divide-y divide-gray-100 dark:divide-gray-500 overflow-y-auto">
<ul class="max-h-80 scroll-py-2 divide-y divide-gray-100 overflow-y-auto">
@foreach($results as $result)
<li class="p-2">
<h2 class="sr-only">{{ __('Search Results') }}</h2> <h2 class="sr-only">{{ __('Search Results') }}</h2>
<ul class="text-sm text-gray-700"> @foreach($results as $result)
<!-- Active: "bg-indigo-600 text-white" --> <li class="text-sm group">
<li class="group flex cursor-default select-none items-center rounded-md px-3 py-2"> <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">
<!-- Active: "text-white", Not Active: "text-gray-400" --> @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"> <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> @endif
<!-- Active: "text-indigo-100", Not Active: "text-gray-400" --> <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> <span class="ml-3 flex-none text-xs font-semibold text-gray-400">Mod</span> </a>
</li>
</ul>
</li> </li>
@endforeach @endforeach
</ul> </ul>
@else @elseif(!$results->count() && $this->query)
<!-- Empty state, show/hide based on command palette state. -->
<div class="px-6 py-14 text-center sm:px-14"> <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"> <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="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" /> <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> </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> </div>
@endif @endif
</div> </div>

View File

@ -35,8 +35,8 @@
<div> <div>
<div class="sm:hidden"> <div class="sm:hidden">
<label for="tabs" class="sr-only">Select a tab</label> <label for="tabs" class="sr-only">Select a tab</label>
<!-- Use an "onChange" listener to redirect the user to the selected tab URL. --> {{-- 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"> <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 selected>Description</option>
<option>Versions</option> <option>Versions</option>
<option>Comments</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"> <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"> <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>Description</span>
<span aria-hidden="true" class="bg-gray-500 absolute inset-x-0 bottom-0 h-0.5"></span> <span aria-hidden="true" class="bg-gray-500 absolute inset-x-0 bottom-0 h-0.5"></span> </a>
</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"> <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>Versions</span>
<span aria-hidden="true" class="bg-transparent absolute inset-x-0 bottom-0 h-0.5"></span> <span aria-hidden="true" class="bg-transparent absolute inset-x-0 bottom-0 h-0.5"></span>
@ -105,13 +104,23 @@
</li> </li>
@endif @endif
@if($mod->contains_ads) @if($mod->contains_ads)
<li class="px-4 py-4 sm:px-0"> <li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
<h3>{{ __('Includes Advertising') }}</h3> <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> </li>
@endif @endif
@if($mod->contains_ai_content) @if($mod->contains_ai_content)
<li class="px-4 py-4 sm:px-0"> <li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
<h3>{{ __('Includes AI Generated Content') }}</h3> <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> </li>
@endif @endif
</ul> </ul>

View File

@ -12,16 +12,14 @@
{{-- Navigation Links --}} {{-- Navigation Links --}}
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"> <div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')"> {{-- <x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">{{ __('Mods') }}</x-nav-link> --}}
{{ __('Mods') }}
</x-nav-link>
</div> </div>
</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"> <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"> <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 d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path> <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> </svg>
</button> </button>
@ -35,21 +33,21 @@
</svg> </svg>
</button> </button>
@guest() @guest
<div class="ml-4"> <div class="ml-4">
<a href="{{ route('login') }}" class="text-sm font-semibold leading-6 text-gray-900">Log in <span aria-hidden="true">&rarr;</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">&rarr;</span></a>
</div> </div>
@endguest @endguest
{{-- Settings Dropdown --}} {{-- Settings Dropdown --}}
<div class="ms-3 relative"> <div class="relative">
<x-dropdown align="right" width="48"> <x-dropdown align="right" width="48">
<x-slot name="trigger"> <x-slot name="trigger">
@auth() @auth
<span class="inline-flex rounded-md"> <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"> <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"> <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"/> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/>
</svg> </svg>
@ -76,10 +74,8 @@
<div class="border-t border-gray-200 dark:border-gray-700"></div> <div class="border-t border-gray-200 dark:border-gray-700"></div>
{{-- Authentication --}}
<form method="POST" action="{{ route('logout') }}" x-data> <form method="POST" action="{{ route('logout') }}" x-data>
@csrf @csrf
<x-dropdown-link href="{{ route('logout') }}" @click.prevent="$root.submit();"> <x-dropdown-link href="{{ route('logout') }}" @click.prevent="$root.submit();">
{{ __('Log Out') }} {{ __('Log Out') }}
</x-dropdown-link> </x-dropdown-link>
@ -104,29 +100,33 @@
{{-- Responsive Navigation Menu --}} {{-- Responsive Navigation Menu --}}
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1"> <div class="pt-2 pb-3 space-y-1">
<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')"> <x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }} {{ __('Dashboard') }}
</x-responsive-nav-link> </x-responsive-nav-link>
@endauth
</div> </div>
{{-- Responsive Settings Options --}} {{-- Responsive Settings Options --}}
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700"> <div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center px-4"> <div class="flex items-center px-4">
@auth() @auth
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos()) @if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
<div class="shrink-0 me-3"> <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> </div>
@endif @endif
<div> <div>
<div class="font-medium text-base text-gray-800 dark:text-gray-300">{{ Auth::user()->name }}</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-sm text-gray-500 dark:text-gray-300">{{ auth()->user()->email }}</div>
</div> </div>
@endauth() @endauth
</div> </div>
<div class="mt-3 space-y-1"> <div class="mt-3 space-y-1">
@auth
{{-- Account Management --}} {{-- Account Management --}}
<x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')"> <x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
{{ __('Profile') }} {{ __('Profile') }}
@ -146,6 +146,7 @@
{{ __('Log Out') }} {{ __('Log Out') }}
</x-responsive-nav-link> </x-responsive-nav-link>
</form> </form>
@endauth
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,6 @@
<?php <?php
use Illuminate\Foundation\Inspiring; use App\Console\Commands\ImportHub;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () { Schedule::command(ImportHub::class)->hourly();
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote')->hourly();