Homepage Progress

Worked on:
- Mod component templating
- Added dark mode styles for homepage
- Added benchmarking to the wolt import command
- Added MySQL natural sort function

Short todo:
- Add updated time to mod component
- Implement naturalsort function into homepage queries and measure performance difference
- Migrate top navigation from old-build
This commit is contained in:
Refringe 2024-05-24 17:06:02 -04:00
parent 3a81afe488
commit 787f796ad7
Signed by: Refringe
GPG Key ID: 7715B85B4A6306ED
33 changed files with 500 additions and 403 deletions

View File

@ -1,4 +1,4 @@
APP_NAME=Laravel
APP_NAME="The Forge"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
@ -12,6 +12,7 @@ APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
# Much higher in production.
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
@ -19,19 +20,22 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
# Only 'mysql' and 'pgsql' are supported due to a 'naturalsort' database function.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
DB_WOLTLAB_CONNECTION=sqlite
# DB_WOLTLAB_HOST=127.0.0.1
# DB_WOLTLAB_PORT=3306
# DB_WOLTLAB_DATABASE=laravel
# DB_WOLTLAB_USERNAME=root
# DB_WOLTLAB_PASSWORD=
# This is only needed if you are running the app:import-woltlab-data command.
# For normal development you should just seed the database with fake data.
DB_WOLTLAB_CONNECTION=mysql
DB_WOLTLAB_HOST=127.0.0.1
DB_WOLTLAB_PORT=3306
DB_WOLTLAB_DATABASE=laravel
DB_WOLTLAB_USERNAME=root
DB_WOLTLAB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120

View File

@ -9,6 +9,7 @@ use App\Models\SptVersion;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
@ -33,21 +34,60 @@ class ImportWoltlabData extends Command
*/
public function handle(): void
{
$this->importUsers();
$this->newLine();
$this->importLicenses();
$totalTime = Benchmark::value(function () {
$loadDataTime = Benchmark::value(function () {
$this->loadData();
});
$this->info('Execution time: '.round($loadDataTime[1], 2).'ms');
$this->newLine();
$this->importSptVersions();
$importUsersTime = Benchmark::value(function () {
$this->importUsers();
});
$this->info('Execution time: '.round($importUsersTime[1], 2).'ms');
$this->newLine();
$importLicensesTime = Benchmark::value(function () {
$this->importLicenses();
});
$this->info('Execution time: '.round($importLicensesTime[1], 2).'ms');
$this->newLine();
$importSptVersionsTime = Benchmark::value(function () {
$this->importSptVersions();
});
$this->info('Execution time: '.round($importSptVersionsTime[1], 2).'ms');
$this->newLine();
$importModsTime = Benchmark::value(function () {
$this->importMods();
});
$this->info('Execution time: '.round($importModsTime[1], 2).'ms');
$this->newLine();
$importModVersionsTime = Benchmark::value(function () {
$this->importModVersions();
});
$this->info('Execution time: '.round($importModVersionsTime[1], 2).'ms');
$this->newLine();
});
$this->newLine();
$this->info('Data imported successfully');
$this->info('Total execution time: '.round($totalTime[1], 2).'ms');
}
protected function loadData(): void
{
// We're just going to dump a few things in memory to escape the N+1 problem.
$this->output->write('Loading data into memory... ');
$this->fileOptionValues = $this->getFileOptionValues();
$this->fileContent = $this->getFileContent();
$this->importMods();
$this->fileVersionContent = $this->getFileVersionContent();
$this->fileVersionLabels = $this->getFileVersionLabels();
$this->importModVersions();
$this->info('Data imported successfully.');
$this->fileVersionContent = $this->getFileVersionContent();
$this->info('Done.');
}
protected function importUsers(): void
@ -84,7 +124,6 @@ class ImportWoltlabData extends Command
}, 'userID');
$this->info('Total users processed: '.$totalInserted);
$this->newLine();
}
protected function importLicenses(): void
@ -112,7 +151,6 @@ class ImportWoltlabData extends Command
}, 'licenseID');
$this->info('Total licenses processed: '.$totalInserted);
$this->newLine();
}
protected function importSptVersions(): void
@ -125,7 +163,7 @@ class ImportWoltlabData extends Command
$insertData[] = [
'hub_id' => $version->labelID,
'version' => $version->label,
'color_class' => $version->cssClassName,
'color_class' => $this->translateColour($version->cssClassName),
];
}
@ -140,7 +178,17 @@ class ImportWoltlabData extends Command
}, 'labelID');
$this->info('Total licenses processed: '.$totalInserted);
$this->newLine();
}
protected function translateColour(string $colour = ''): string
{
return match ($colour) {
'green' => 'green',
'slightly-outdated' => 'lime',
'yellow' => 'yellow',
'red' => 'red',
default => 'gray',
};
}
protected function importMods(): void
@ -155,20 +203,20 @@ class ImportWoltlabData extends Command
DB::connection('mysql_woltlab')->table('filebase1_file')->chunkById(100, function (Collection $mods) use (&$command, &$curl, &$totalInserted) {
foreach ($mods as $mod) {
if ($mod->fileID == 1 || $mod->fileID == 116 || $mod->fileID == 480) {
// These are special cases that we don't want to import. Installers, base files, and the patchers.
continue;
}
$modContent = $this->fileContent[$mod->fileID] ?? [];
$modOptions = $this->fileOptionValues[$mod->fileID] ?? [];
$versionLabel = $this->fileVersionLabels[$mod->fileID] ?? [];
if (empty($versionLabel)) {
continue;
}
$insertData[] = [
'hub_id' => $mod->fileID,
'user_id' => User::whereHubId($mod->userID)->value('id'),
'name' => $modContent ? $modContent->subject : '',
'slug' => $modContent ? Str::slug($modContent->subject) : '',
'teaser' => $modContent ? $modContent->teaser : '',
'teaser' => $modContent ? (strlen($modContent->teaser) > 100 ? Str::take($modContent->teaser, 97).'...' : $modContent->teaser) : '',
'description' => $modContent ? $modContent->message : '',
'thumbnail' => $this->fetchModThumbnail($command, $curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
@ -194,7 +242,6 @@ class ImportWoltlabData extends Command
curl_close($curl);
$this->info('Total mods processed: '.$totalInserted);
$this->newLine();
}
protected function getFileOptionValues(): array
@ -331,7 +378,7 @@ class ImportWoltlabData extends Command
$command = $this;
$totalInserted = 0;
DB::connection('mysql_woltlab')->table('filebase1_file_version')->chunkById(100, function (Collection $versions) use (&$command, &$totalInserted) {
DB::connection('mysql_woltlab')->table('filebase1_file_version')->chunkById(500, function (Collection $versions) use (&$command, &$totalInserted) {
foreach ($versions as $version) {
$versionContent = $this->fileVersionContent[$version->versionID] ?? [];
@ -369,7 +416,6 @@ class ImportWoltlabData extends Command
}, 'versionID');
$this->info('Total mod versions processed: '.$totalInserted);
$this->newLine();
}
protected function fetchVirusTotalLink(array $options): string

View File

@ -1,16 +0,0 @@
<?php
namespace App\Helpers;
class ColorHelper
{
public static function tagColorClasses($color): string
{
return match ($color) {
'red' => 'bg-red-50 text-red-700 ring-red-600/20',
'green' => 'bg-green-50 text-green-700 ring-green-600/20',
'yellow' => 'bg-yellow-50 text-yellow-700 ring-yellow-600/20',
default => 'bg-gray-50 text-gray-700 ring-gray-600/20',
};
}
}

View File

@ -23,6 +23,14 @@ class Mod extends Model
'source_code_link',
];
protected function slug(): Attribute
{
return Attribute::make(
get: fn (string $value) => strtolower($value),
set: fn (string $value) => Str::slug($value),
);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
@ -38,21 +46,54 @@ class Mod extends Model
return $this->hasMany(ModVersion::class);
}
public function versionLastUpdated()
public function scopeWithTotalDownloads($query)
{
return $this->hasOne(ModVersion::class)->lastUpdated();
$query->addSelect(['total_downloads' => ModVersion::selectRaw('SUM(downloads) AS total_downloads')
->whereColumn('mod_id', 'mods.id'),
]);
}
public function versionLatestSptVersion()
public function latestSptVersion(): BelongsTo
{
return $this->hasOne(ModVersion::class)->latestSptVersion();
return $this->belongsTo(ModVersion::class, 'latest_spt_version_id');
}
protected function slug(): Attribute
public function scopeWithLatestSptVersion($query)
{
return Attribute::make(
get: fn (string $value) => strtolower($value),
set: fn (string $value) => Str::slug($value),
);
return $query
->addSelect(['latest_spt_version_id' => ModVersion::select('id')
->whereColumn('mod_id', 'mods.id')
->orderByDesc(
SptVersion::select('version')
->whereColumn('mod_versions.spt_version_id', 'spt_versions.id')
->orderByDesc('version')
->take(1),
)
->orderByDesc('version')
->take(1),
])
->with(['latestSptVersion', 'latestSptVersion.sptVersion']);
}
public function lastUpdatedVersion(): BelongsTo
{
return $this->belongsTo(ModVersion::class, 'last_updated_spt_version_id');
}
public function scopeWithLastUpdatedVersion($query)
{
return $query
->addSelect(['last_updated_spt_version_id' => ModVersion::select('id')
->whereColumn('mod_id', 'mods.id')
->orderByDesc('updated_at')
->take(1),
])
->orderByDesc(
ModVersion::select('updated_at')
->whereColumn('mod_id', 'mods.id')
->orderByDesc('updated_at')
->take(1)
)
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion']);
}
}

View File

@ -2,7 +2,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -26,20 +25,8 @@ class ModVersion extends Model
return $this->belongsTo(Mod::class);
}
public function sptVersion(): belongsTo
public function sptVersion(): BelongsTo
{
return $this->belongsTo(SptVersion::class);
}
public function scopeLastUpdated(Builder $query): void
{
$query->orderByDesc('created_at');
}
public function scopeLatestSptVersion(Builder $query): void
{
$query->orderByDesc(
SptVersion::select('spt_versions.version')->whereColumn('mod_versions.spt_version_id', 'spt_versions.id')
)->orderByDesc('mod_versions.version');
}
}

View File

@ -2,13 +2,13 @@
namespace App\View\Components;
use App\Helpers\ColorHelper;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\View\Component;
class ModList extends Component
{
public $mods;
public Collection $mods;
public string $versionScope;
@ -16,11 +16,6 @@ class ModList extends Component
{
$this->mods = $mods;
$this->versionScope = $versionScope;
foreach ($this->mods as $mod) {
$color = $mod->{$this->versionScope}->sptVersion->color_class;
$mod->colorClass = ColorHelper::tagColorClasses($color);
}
}
public function render(): View

View File

@ -24,17 +24,36 @@ class ModListSection extends Component
private function fetchFeaturedMods(): Collection
{
return Mod::with('versionLatestSptVersion.sptVersion')->whereFeatured(true)->take(6)->get();
return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
->withLatestSptVersion()
->withTotalDownloads()
->with('user:id,name')
->where('featured', true)
->latest()
->limit(6)
->get();
}
private function fetchLatestMods(): Collection
{
return Mod::with('versionLatestSptVersion.sptVersion')->latest()->take(6)->get();
return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
->withLatestSptVersion()
->withTotalDownloads()
->with('user:id,name')
->latest()
->limit(6)
->get();
}
private function fetchUpdatedMods(): Collection
{
return Mod::with('versionLastUpdated.sptVersion')->take(6)->get();
return Mod::select(['id', 'user_id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
->withLastUpdatedVersion()
->withTotalDownloads()
->with('user:id,name')
->latest()
->limit(6)
->get();
}
public function getSections(): array
@ -43,17 +62,17 @@ class ModListSection extends Component
[
'title' => 'Featured Mods',
'mods' => $this->modsFeatured,
'versionScope' => 'versionLatestSptVersion',
'versionScope' => 'latestSptVersion',
],
[
'title' => 'Latest Mods',
'title' => 'Newest Mods',
'mods' => $this->modsLatest,
'versionScope' => 'versionLatestSptVersion',
'versionScope' => 'latestSptVersion',
],
[
'title' => 'Recently Updated Mods',
'mods' => $this->modsUpdated,
'versionScope' => 'versionLastUpdated',
'versionScope' => 'lastUpdatedVersion',
],
];
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\View\Components;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ModVersionTag extends Component
{
public string $tagColor;
public function __construct($tagColor)
{
$this->tagColor = $tagColor;
}
public function render(): View
{
return view('components.mod-version-tag');
}
public function tagClasses($tagColor): string
{
return match ($this->tagColor) {
'red' => 'bg-red-50 text-red-700 ring-red-600/20',
'green' => 'bg-green-50 text-green-700 ring-green-600/20',
'yellow' => 'bg-yellow-50 text-yellow-700 ring-yellow-600/20',
default => 'bg-gray-50 text-gray-700 ring-gray-600/20',
};
}
}

View File

@ -33,10 +33,7 @@
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
},
"files": [
"app/Helpers/ColorHelper.php"
]
}
},
"autoload-dev": {
"psr-4": {

View File

@ -16,7 +16,7 @@ return [
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------

View File

@ -13,7 +13,7 @@ return new class extends Migration
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('hub_id')->nullable()->unique()->default(null);
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();

View File

@ -10,7 +10,7 @@ return new class extends Migration
{
Schema::create('licenses', function (Blueprint $table) {
$table->id();
$table->string('hub_id')->nullable()->unique()->default(null);
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->string('name');
$table->string('link');
$table->softDeletes();

View File

@ -12,7 +12,7 @@ return new class extends Migration
{
Schema::create('mods', function (Blueprint $table) {
$table->id();
$table->string('hub_id')->nullable()->unique()->default(null);
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->foreignIdFor(User::class)->constrained('users');
$table->string('name');
$table->string('slug');

View File

@ -10,7 +10,7 @@ return new class extends Migration
{
Schema::create('spt_versions', function (Blueprint $table) {
$table->id();
$table->string('hub_id')->nullable()->unique()->default(null);
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->string('version');
$table->string('color_class');
$table->softDeletes();

View File

@ -12,7 +12,7 @@ return new class extends Migration
{
Schema::create('mod_versions', function (Blueprint $table) {
$table->id();
$table->string('hub_id')->nullable()->unique()->default(null);
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->foreignIdFor(Mod::class)->constrained('mods');
$table->string('version');
$table->longText('description');

View File

@ -0,0 +1,79 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
if (config('database.default') === 'sqlite') {
throw new \Exception('This project does not support SQLite. Update to MySQL or PostgreSQL.');
}
if (config('database.default') === 'mysql') {
// https://www.drupal.org/project/natsort
DB::unprepared("
DROP FUNCTION IF EXISTS naturalsort;
CREATE FUNCTION naturalsort (s VARCHAR (255)) RETURNS VARCHAR (255) NO SQL DETERMINISTIC BEGIN
DECLARE orig VARCHAR (255) DEFAULT s;
DECLARE ret VARCHAR (255) DEFAULT '';
IF s IS NULL THEN
RETURN NULL;
ELSEIF NOT s REGEXP '[0-9]' THEN
SET ret = s;
ELSE
SET s = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(s, '0', '#'), '1', '#'), '2', '#'), '3', '#'), '4', '#');
SET s = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(s, '5', '#'), '6', '#'), '7', '#'), '8', '#'), '9', '#');
SET s = REPLACE(s, '.#', '##');
SET s = REPLACE(s, '#,#', '###');
BEGIN
DECLARE numpos INT;
DECLARE numlen INT;
DECLARE numstr VARCHAR (255);
lp1: LOOP
SET numpos = locate('#', s);
IF numpos = 0 THEN
SET ret = concat(ret, s);
LEAVE lp1;
END IF;
SET ret = concat(ret, substring(s, 1, numpos - 1));
SET s = substring(s, numpos);
SET orig = substring(orig, numpos);
SET numlen = char_length(s) - char_length(trim(LEADING '#' FROM s));
SET numstr = cast(REPLACE(substring(orig, 1, numlen), ',', '') AS DECIMAL (13, 3));
SET numstr = lpad(numstr, 15, '0');
SET ret = concat(ret, '[', numstr, ']');
SET s = substring(s, numlen + 1);
SET orig = substring(orig, numlen + 1);
END LOOP;
END;
END IF;
SET ret = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ret, ' ', ''), ',', ''), ':', ''), '.', ''), ';', '' ), '(', ''), ')', '');
RETURN ret;
END;
");
}
if (config('database.default') === 'pgsql') {
// http://www.rhodiumtoad.org.uk/junk/naturalsort.sql
DB::unprepared('
create or replace function naturalsort(text)
returns bytea language sql immutable strict as
$f$ select string_agg(convert_to(coalesce(r[2],length(length(r[1])::text) || length(r[1])::text || r[1]),\'SQL_ASCII\'),\'\x00\')
from regexp_matches($1, \'0*([0-9]+)|([^0-9]+)\', \'g\') r; $f$;
');
}
}
public function down(): void
{
if (config('database.default') === 'sqlite') {
throw new \Exception('This project does not support SQLite. Update to MySQL or PostgreSQL.');
}
if (config('database.default') === 'mysql' || config('database.default') === 'pgsql') {
DB::unprepared('DROP FUNCTION IF EXISTS naturalsort');
}
}
};

View File

@ -8,9 +8,33 @@
button[type="submit"]:not([role="menuitem"]),
button[type="button"]:not([role="menuitem"]) {
@apply border border-transparent rounded-md text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-600
@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
}
input[type="checkbox"] {
@apply text-gray-800 focus:ring-gray-600 border-gray-300 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;
}
.badge-version {
@apply bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-100;
&.red {
@apply bg-red-100 dark:bg-red-700 text-red-700 dark:text-red-100;
}
&.green {
@apply bg-green-100 dark:bg-green-700 text-green-700 dark:text-green-100;
}
&.emerald {
@apply bg-emerald-100 dark:bg-emerald-700 text-emerald-700 dark:text-emerald-100;
}
&.lime {
@apply bg-lime-100 dark:bg-lime-700 text-lime-700 dark:text-lime-100;
}
&.yellow {
@apply bg-yellow-100 dark:bg-yellow-700 text-yellow-700 dark:text-yellow-100;
}
}

View File

@ -1 +1,37 @@
import './bootstrap';
import "./bootstrap";
var themeToggleDarkIcon = document.getElementById("theme-toggle-dark-icon");
var themeToggleLightIcon = document.getElementById("theme-toggle-light-icon");
if (
localStorage.getItem("color-theme") === "dark" ||
(!("color-theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
themeToggleLightIcon.classList.remove("hidden");
} else {
themeToggleDarkIcon.classList.remove("hidden");
}
var themeToggleBtn = document.getElementById("theme-toggle");
themeToggleBtn.addEventListener("click", function () {
themeToggleDarkIcon.classList.toggle("hidden");
themeToggleLightIcon.classList.toggle("hidden");
if (localStorage.getItem("color-theme")) {
if (localStorage.getItem("color-theme") === "light") {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
}
} else {
if (document.documentElement.classList.contains("dark")) {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
} else {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
}
}
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 84 B

View File

@ -1 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@ -0,0 +1,49 @@
<footer class="bg-gray-900" aria-labelledby="footer-heading">
<h2 id="footer-heading" class="sr-only">Footer</h2>
<div class="mx-auto max-w-7xl px-6 pb-8 pt-14 sm:pt-20 lg:px-8 lg:pt-29">
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
<div>
<p class="text-lg italic font-extrabold leading-6 text-white">The Forge</p>
</div>
<div class="mt-16 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 xl:col-span-2 xl:mt-0">
<div class="sm:order-first">
<h3 class="text-sm font-semibold leading-6 text-white">Single Player Tarkov</h3>
<ul role="list" class="mt-6 space-y-4">
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">About</a></li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Articles</a></li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Documentation</a></li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Mods</a></li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Support</a></li>
</ul>
</div>
<div class="sm:order-last">
<h3 class="text-sm font-semibold leading-6 text-white">Escape from Tarkov</h3>
<ul role="list" class="mt-6 space-y-4">
<li>
<a href="https://www.escapefromtarkov.com/preorder-page" class="text-sm leading-6 text-gray-300 hover:text-white">Purchase</a>
</li>
<li>
<a href="https://escapefromtarkov.fandom.com/wiki/Escape_from_Tarkov_Wiki" class="text-sm leading-6 text-gray-300 hover:text-white">Wiki</a>
</li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold leading-6 text-white">Legal</h3>
<ul role="list" class="mt-6 space-y-4">
<li>
<a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Community Guidelines</a>
</li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Terms of Service</a>
</li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Privacy Policy</a></li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">Disclaimer</a></li>
<li><a href="#" class="text-sm leading-6 text-gray-300 hover:text-white">DMCA</a></li>
</ul>
</div>
</div>
</div>
<div class="mt-8 border-t border-white/10 pt-8 md:flex md:items-center md:justify-between">
<p class="mt-8 text-xs leading-5 text-gray-400 md:order-1 md:mt-0">&copy; {{ date('Y') }} {{ config('app.name', 'The Forge') }}. All rights reserved.</p>
</div>
</div>
</footer>

View File

@ -1,40 +1,40 @@
@props(['mods', 'versionScope'])
<ul role="list" {{ $attributes->class(['grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3']) }}>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
@foreach ($mods as $mod)
<li class="col-span-1 divide-y divide-gray-200 rounded-lg bg-white shadow">
<div class="flex w-full items-center justify-between space-x-6 p-4">
<img class="h-16 w-16 flex-shrink-0 rounded-[5px] bg-gray-300" src="https://placehold.co/300x300/EEE/31343C?font=open-sans&text=MOD" alt="">
<div class="flex-1 truncate">
<div class="flex items-center space-x-3">
<h3 class="truncate text-sm font-medium text-gray-900">{{ $mod->name }}</h3>
<span class="{{ $mod->colorClass }} inline-flex flex-shrink-0 items-center rounded-full px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset">
{{ $mod->{$versionScope}->sptVersion->version }}
</span>
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}">
<div class="flex flex-col group h-full w-full max-w-md mx-auto bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl overflow-hidden md:max-w-2xl hover:shadow-lg hover:bg-gray-50 dark:hover:bg-black hover:shadow-gray-400 dark:hover:shadow-black transition-all duration-200">
<div class="h-auto md:h-full md:flex">
<div class="h-auto md:h-full md:shrink-0 overflow-hidden">
@if(empty($mod->thumbnail))
<img src="https://placehold.co/450x450/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
<img src="https://placehold.co/450x450/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
@else
<img src="{{ $mod->thumbnail }}" alt="{{ $mod->name }}" class="h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
@endif
</div>
<p class="mt-1 truncate text-sm text-gray-500">{{ $mod->description }}</p>
</div>
</div>
<div>
<div class="-mt-px flex divide-x divide-gray-200">
<div class="flex w-0 flex-1">
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}" class="relative -mr-px inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-bl-lg border border-t-0 border-transparent py-4 text-sm font-semibold text-gray-900">
<svg class="h-5 w-5 text-gray-400" 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 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.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/>
</svg>
{{ __('View Details') }}
</a>
</div>
<div class="-ml-px flex w-0 flex-1">
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}/download" class="relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-t-0 border-transparent py-4 text-sm font-semibold text-gray-900">
<svg class="h-5 w-5 text-gray-400" 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="M12 9.75v6.75m0 0-3-3m3 3 3-3m-8.25 6a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z"/>
</svg>
{{ __('Download') }}
</a>
<div class="flex flex-col w-full justify-between p-5">
<div>
<div class="flex justify-between items-center space-x-3">
<h3 class="block mt-1 text-lg leading-tight font-medium text-black dark:text-white group-hover:underline">{{ $mod->name }}</h3>
<span class="badge-version {{ $mod->{$versionScope}->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
{{ $mod->{$versionScope}->sptVersion->version }}
</span>
</div>
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ $mod->teaser }}</p>
</div>
<p class="text-slate-700 dark:text-gray-300 text-sm">
<span>By {{ $mod->user->name }}</span>
@if (!is_null($mod->total_downloads))
<span>&ndash; {{ Number::format($mod->total_downloads) }} downloads</span>
@endif
@if(!is_null($mod->updated_at))
<span>&ndash; updated {{ $mod->updated_at }}</span>
@endif
</p>
</div>
</div>
</div>
</li>
</a>
@endforeach
</ul>
</div>

View File

@ -1,9 +1,9 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-gray-800 text-sm font-medium leading-5 text-gray-900 dark:text-gray-300 focus:outline-none focus:border-indigo-700 dark:focus:border-gray-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent dark:border-gray-800 text-sm font-medium leading-5 text-gray-500 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-500 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-500 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>

View File

@ -1,11 +1,11 @@
<div {{ $attributes->class(['md:flex md:items-center md:justify-between border-b pb-4 mb-6']) }}>
<div {{ $attributes->class(['md:flex md:items-center md:justify-between border-b dark:border-b-gray-800 pb-4 mb-6']) }}>
<div class="min-w-0 flex-1">
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">{{ __($title) }}</h2>
<h2 class="text-2xl font-bold leading-7 text-gray-900 dark:text-gray-200 sm:truncate sm:text-3xl sm:tracking-tight">{{ __($title) }}</h2>
</div>
@if (isset($buttonText) && isset($buttonLink))
<div class="mt-4 flex md:ml-4 md:mt-0">
<a href="{{ $buttonLink }}">
<button type="button" class="ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">{{ __($buttonText) }}</button>
<button type="button">{{ __($buttonText) }}</button>
</a>
</div>
@endif

View File

@ -1,9 +1,9 @@
@props(['active'])
@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 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-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 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
$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-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
<a {{ $attributes->merge(['class' => $classes]) }}>

View File

@ -1,4 +1,4 @@
<div class="items-center justify-between gap-x-6 bg-gray-900 px-6 py-2.5 sm:pr-3.5 lg:pl-8">
<p class="text-center text-sm leading-6 text-white">Notice: The Forge is currently under
<div class="items-center justify-between gap-x-6 text-gray-200 bg-gray-900 dark:text-gray-900 dark:bg-gray-100 px-6 py-2.5 sm:pr-3.5 lg:pl-8">
<p class="text-center text-sm leading-6">Notice: The Forge is currently under
<em class="font-bold">heavy</em> construction. Expect nothing to work.</p>
</div>

View File

@ -1,29 +1,26 @@
<x-app-layout>
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
{{-- Welcome Section --}}
<div class="relative isolate overflow-hidden bg-gray-900 px-6 py-24 sm:py-32 lg:px-8 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>
<div class="hidden sm:absolute sm:-top-10 sm:right-1/2 sm:-z-10 sm:mr-10 sm:block sm:transform-gpu sm:blur-3xl" aria-hidden="true">
<div class="aspect-[1097/845] w-[68.5625rem] bg-gradient-to-tr from-[#333] to-[#000] opacity-20" style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)"></div>
</div>
<div class="absolute -top-52 left-1/2 -z-10 -translate-x-1/2 transform-gpu blur-3xl sm:top-[-28rem] sm:ml-16 sm:translate-x-0 sm:transform-gpu" aria-hidden="true">
<div class="aspect-[1097/845] w-[68.5625rem] bg-gradient-to-tr from-[#333] to-[#000] opacity-20" style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)"></div>
</div>
<div class="mx-auto max-w-2xl text-center">
<h2 class="text-4xl font-bold tracking-tight text-white sm:text-6xl drop-shadow-md">Step into
<em>{{ config('app.name', 'The Forge') }}</em></h2>
<p class="mt-6 text-lg leading-8 text-gray-300 drop-shadow-md">The greatest resource available for Single Player Tarkov modifications. Where modding legends are made. Discover powerful tools, expert-written guides, and exclusive mods. Craft your vision. Transform the game.</p>
</div>
<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">
<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>
<div class="hidden sm:absolute sm:-top-10 sm:right-1/2 sm:-z-10 sm:mr-10 sm:block sm:transform-gpu sm:blur-3xl" aria-hidden="true">
<div class="aspect-[1097/845] w-[68.5625rem] bg-gradient-to-tr from-[#333] to-[#000] opacity-20" style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)"></div>
</div>
<div class="absolute -top-52 left-1/2 -z-10 -translate-x-1/2 transform-gpu blur-3xl sm:top-[-28rem] sm:ml-16 sm:translate-x-0 sm:transform-gpu" aria-hidden="true">
<div class="aspect-[1097/845] w-[68.5625rem] bg-gradient-to-tr from-[#333] to-[#000] opacity-20" style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)"></div>
</div>
<div class="mx-auto max-w-2xl text-center">
<h2 class="text-4xl font-bold tracking-tight text-white sm:text-6xl drop-shadow-md">Step into
<em class="dark:text-gray-400">{{ config('app.name', 'The Forge') }}</em></h2>
<p class="mt-6 text-lg leading-8 text-gray-300 drop-shadow-md">The greatest resource available for Single Player Tarkov modifications. Where modding legends are made. Discover powerful tools, expert-written guides, and exclusive mods. Craft your vision. Transform the game.</p>
</div>
</div>
<div class="pb-10">
<x-mod-list-section/>
</div>
<div class="pb-10">
<x-mod-list-section/>
</div>
</div>
</div>

View File

@ -7,14 +7,19 @@
<title>{{ config('app.name', 'The Forge') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet"/>
<link href="https://fonts.bunny.net" rel="preconnect">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet">
{{-- Handle setting the dark mode theme. Done here to avoid FOUC --}}
<script>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<!-- Styles -->
@livewireStyles
</head>
<body class="font-sans antialiased">
@ -23,24 +28,24 @@
<x-banner/>
<div class="min-h-screen bg-gray-100">
<div class="min-h-screen bg-gray-100 dark:bg-gray-800">
@livewire('navigation-menu')
<!-- Page Heading -->
@if (isset($header))
<header class="bg-white shadow">
<header class="bg-white dark:bg-gray-800 shadow dark:shadow-white">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endif
<!-- Page Content -->
<main>
<main class="py-12">
{{ $slot }}
</main>
</div>
<x-footer/>
@stack('modals')
@livewireScripts

View File

@ -1,27 +1,32 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<link href="https://fonts.bunny.net" rel="preconnect">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet">
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
{{-- Handle setting the dark mode theme. Done here to avoid FOUC --}}
<script>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<!-- Styles -->
@livewireStyles
</head>
<body>
<div class="font-sans text-gray-900 antialiased">
{{ $slot }}
</div>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body>
<div class="font-sans text-gray-900 antialiased">
{{ $slot }}
</div>
@livewireScripts
</body>
@livewireScripts
</body>
</html>

View File

@ -1,54 +0,0 @@
<div class="pb-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="border-b border-gray-200 bg-white px-4 py-5 sm:px-6">
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
<div class="ml-4 mt-2">
<h3 class="text-base font-semibold leading-6 text-gray-900">{{ __('Latest Mods') }}</h3>
</div>
<div class="ml-4 mt-2 flex-shrink-0">
<button type="button" class="relative inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">{{ __('View All') }}</button>
</div>
</div>
</div>
<ul role="list" class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($latestMods as $mod)
<li class="col-span-1 divide-y divide-gray-200 rounded-lg bg-white shadow">
<div class="flex w-full items-center justify-between space-x-6 p-6">
<div class="flex-1 truncate">
<div class="flex items-center space-x-3">
<h3 class="truncate text-sm font-medium text-gray-900">{{ $mod->name }}</h3>
<span class="inline-flex flex-shrink-0 items-center rounded-full bg-{{ $mod->spt_version_color_class }}-50 px-1.5 py-0.5 text-xs font-medium text-{{ $mod->spt_version_color_class }}-700 ring-1 ring-inset ring-{{ $mod->spt_version_color_class }}-600/20">{{ $mod->spt_version }}</span>
</div>
<p class="mt-1 truncate text-sm text-gray-500">{{ $mod->description }}</p>
</div>
<img class="h-10 w-10 flex-shrink-0 rounded-[5px] bg-gray-300" src="https://placehold.co/300x300/EEE/31343C?font=open-sans&text=MOD" alt="">
</div>
<div>
<div class="-mt-px flex divide-x divide-gray-200">
<div class="flex w-0 flex-1">
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}" class="relative -mr-px inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-gray-900">
<svg class="h-5 w-5 text-gray-400" 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 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.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/>
</svg>
{{ __('View Details') }}
</a>
</div>
<div class="-ml-px flex w-0 flex-1">
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}/download" class="relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-gray-900">
<svg class="h-5 w-5 text-gray-400" 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="M12 9.75v6.75m0 0-3-3m3 3 3-3m-8.25 6a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z"/>
</svg>
{{ __('Download') }}
</a>
</div>
</div>
</div>
</li>
@endforeach
</ul>
</div>
</div>
</div>

View File

@ -1,16 +1,16 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-700">
{{-- Primary Navigation Menu --}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
{{-- Logo --}}
<div class="shrink-0 flex items-center">
<a href="{{ route('home') }}">
<x-application-mark class="block h-9 w-auto"/>
</a>
</div>
<!-- Navigation Links -->
{{-- 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') }}
@ -19,70 +19,29 @@
</div>
<div class="hidden sm:flex sm:items-center sm:ms-6">
<!-- Teams Dropdown -->
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
<div class="ms-3 relative">
<x-dropdown align="right" width="60">
<x-slot name="trigger">
<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 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
{{ Auth::user()->currentTeam->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="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"/>
</svg>
</button>
</span>
</x-slot>
<x-slot name="content">
<div class="w-60">
<!-- Team Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Manage Team') }}
</div>
<!-- Team Settings -->
<x-dropdown-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}">
{{ __('Team Settings') }}
</x-dropdown-link>
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
<x-dropdown-link href="{{ route('teams.create') }}">
{{ __('Create New Team') }}
</x-dropdown-link>
@endcan
<!-- Team Switcher -->
@if (Auth::user()->allTeams()->count() > 1)
<div class="border-t border-gray-200"></div>
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Switch Teams') }}
</div>
@foreach (Auth::user()->allTeams() as $team)
<x-switchable-team :team="$team"/>
@endforeach
@endif
</div>
</x-slot>
</x-dropdown>
</div>
@endif
<!-- Settings Dropdown -->
{{-- Settings Dropdown --}}
<div class="ms-3 relative">
{{-- Theme Toggle Button --}}
<button id="theme-toggle" type="button" 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>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</button>
<x-dropdown align="right" width="48">
<x-slot name="trigger">
@auth()
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
<button class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
<button class="flex text-sm border-2 border-transparent dark:border-gray-700 rounded-full focus:outline-none focus:border-gray-300 dark:focus:border-gray-700 transition">
<img class="h-8 w-8 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}"/>
</button>
@else
<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 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 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 }}
<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">
@ -95,8 +54,8 @@
</x-slot>
<x-slot name="content">
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
{{-- Account Management --}}
<div class="block px-4 py-2 text-xs text-gray-400 dark:text-gray-300">
{{ __('Manage Account') }}
</div>
@ -110,9 +69,9 @@
</x-dropdown-link>
@endif
<div class="border-t border-gray-200"></div>
<div class="border-t border-gray-200 dark:border-gray-700"></div>
<!-- Authentication -->
{{-- Authentication --}}
<form method="POST" action="{{ route('logout') }}" x-data>
@csrf
@ -125,9 +84,9 @@
</div>
</div>
<!-- Hamburger -->
{{-- Hamburger --}}
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 focus:text-gray-500 dark:focus:text-gray-300 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@ -137,7 +96,7 @@
</div>
</div>
<!-- Responsive Navigation Menu -->
{{-- 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')">
@ -145,8 +104,8 @@
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
{{-- 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()
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
@ -156,14 +115,14 @@
@endif
<div>
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ 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()
</div>
<div class="mt-3 space-y-1">
<!-- Account Management -->
{{-- Account Management --}}
<x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
{{ __('Profile') }}
</x-responsive-nav-link>
@ -174,7 +133,7 @@
</x-responsive-nav-link>
@endif
<!-- Authentication -->
{{-- Authentication --}}
<form method="POST" action="{{ route('logout') }}" x-data>
@csrf
@ -182,39 +141,6 @@
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
<!-- Team Management -->
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
<div class="border-t border-gray-200"></div>
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Manage Team') }}
</div>
<!-- Team Settings -->
<x-responsive-nav-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}" :active="request()->routeIs('teams.show')">
{{ __('Team Settings') }}
</x-responsive-nav-link>
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
<x-responsive-nav-link href="{{ route('teams.create') }}" :active="request()->routeIs('teams.create')">
{{ __('Create New Team') }}
</x-responsive-nav-link>
@endcan
<!-- Team Switcher -->
@if (Auth::user()->allTeams()->count() > 1)
<div class="border-t border-gray-200"></div>
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Switch Teams') }}
</div>
@foreach (Auth::user()->allTeams() as $team)
<x-switchable-team :team="$team" component="responsive-nav-link"/>
@endforeach
@endif
@endif
</div>
</div>
</div>

View File

@ -7,7 +7,7 @@ Route::get('/', function () {
})->name('home');
Route::get('/mods', function () {
return 'list all mods';
return '';
})->name('mods');
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {

View File

@ -4,6 +4,8 @@ import typography from "@tailwindcss/typography";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "selector",
content: [
"./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php",
"./vendor/laravel/jetstream/**/*.blade.php",
@ -19,11 +21,5 @@ export default {
},
},
safelist: [
{
pattern: /(bg|text|ring)-(green|yellow|red|grey)-(50|700|600\/20)/,
},
],
plugins: [forms, typography],
};