Merge branch 'develop'

This commit is contained in:
Refringe 2024-07-03 17:51:28 -04:00
commit e8f2562005
Signed by: Refringe
SSH Key Fingerprint: SHA256:t865XsQpfTeqPRBMN2G6+N8wlDjkgUCZF3WGW6O9N/k
218 changed files with 3718 additions and 2784 deletions

View File

@ -43,9 +43,8 @@ DB_HUB_PASSWORD=
DB_HUB_CHARSET=utf8mb4
DB_HUB_COLLATION=utf8mb4_0900_ai_ci
SESSION_DRIVER=redis
SESSION_STORE=redis
SESSION_CONNECTION=default
SESSION_DRIVER=database
SESSION_CONNECTION=mysql
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
@ -82,8 +81,6 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
MAIL_FROM_NAME="${APP_NAME}"
NOVA_LICENSE_KEY=
OCTANE_SERVER=frankenphp
OCTANE_HTTPS=true

44
.env.light Normal file
View File

@ -0,0 +1,44 @@
APP_NAME="The Forge"
APP_ENV=local
# Generate a new key with: `php artisan key:generate`
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
# Update to whatever you've configured your local web server to use.
APP_URL=http://forge.test
VITE_APP_NAME="${APP_NAME}"
# Much higher in production
BCRYPT_ROUNDS=4
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
# SQLite is being used here for simplicity, but will break functionality with
# the import job. Shouldn't matter for light development work. Must be absolute!
DB_CONNECTION=sqlite
DB_DATABASE=/Users/USER/Sites/forge/database/database.sqlite
FILESYSTEM_DISK=local
ASSET_URL="${APP_URL}/storage"
ASSET_URL_LIVEWIRE=/vendor/livewire/livewire.js
SESSION_DRIVER=file
BROADCAST_CONNECTION=log
CACHE_STORE=file
QUEUE_CONNECTION=sync
SCOUT_DRIVER=collection
# Mail will be written to the file log.
MAIL_MAILER=log
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
MAIL_FROM_NAME="${APP_NAME}"

24
.github/README.md vendored
View File

@ -12,11 +12,7 @@ The Forge is a Laravel-based web application that provides a platform for the Si
## Development Environment Setup
This is a [Laravel](https://laravel.com/docs/11.x) project that uses [Sail](https://laravel.com/docs/11.x/sail), which provides a Docker-based development environment. Ensure you review the Sail documentation for useage, particularly in a [Windows environment](https://laravel.com/docs/11.x/installation#sail-on-windows), as WSL2 is recommended.
### Accessing the Application:
Once the Docker containers are running with Sail you can access the application at <https://localhost>.
We use [Laravel Sail](https://laravel.com/docs/11.x/sail) to mirror the services that are used in our production server in a local development environment. You can see detailed instructions on how to configure the [full development environment](https://github.com/sp-tarkov/forge/wiki/Full-Windows-Dev-Env) or a [lightweight development environment](https://github.com/sp-tarkov/forge/wiki/Light-Windows-Dev-Env) on the project wiki. The full development environment is recommended.
### Available Services:
@ -31,15 +27,13 @@ Once the Docker containers are running with Sail you can access the application
| Service | Authentication | Access Via Host |
|----------------------------------|----------------|-----------------------------|
| Administration Panel (Nova*) | Via User Role | <https://localhost/nova> |
| Laravel Filament Admin Panel | Via User Role | <https://localhost/admin> |
| Redis Queue Management (Horizon) | Via User Role | <https://localhost/horizon> |
| Website Status (Pulse) | Via User Role | <https://localhost/pulse> |
| Meilisearch WebUI | Local Only | <http://localhost:7700> |
| Mailpit WebUI | Local Only | <http://localhost:8025> |
<sup>*Nova may be replaced shortly due to License issues.</sup>
Most of these connection settings should already be configured in the `.env.example` file. Simply save the `.env.example` file as `.env` and adjust further settings as needed.
Most of these connection settings should already be configured in the `.env.full` or `.env.light` example files. Simply save one of these (depending on your environment) as `.env` and adjust settings as needed.
### Basic Usage Examples
@ -47,32 +41,32 @@ Here are some basic commands to get started with Forge:
```
# Start the Docker containers in detached mode:
./vendor/bin/sail up -d
sail up -d
```
```
# View all of the available Artisan commands:
./vendor/bin/sail artisan
sail artisan
```
```
# Migrate and seed the database with test data:
./vendor/bin/sail artisan migrate:fresh seed
sail artisan migrate:fresh seed
```
```
# Run Laravel Horizon (the queue workers/monitor):
./vendor/bin/sail artisan horizon
sail artisan horizon
```
```
# Install NPM dependencies from within the container:
./vendor/bin/sail npm install
sail npm install
```
```
# Start the development server:
./vendor/bin/sail npm run dev
sail npm run dev
```
### More Information

View File

@ -22,12 +22,6 @@ jobs:
php-version: '8.3'
extensions: mbstring, dom, fileinfo
coverage: none
- name: Configure Laravel Nova Authentication
shell: bash
env:
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
run: composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
@ -43,10 +37,7 @@ jobs:
run: |
php -r "file_exists('.env') || copy('.env.ci', '.env');"
php artisan key:generate
php artisan config:cache
php artisan route:cache
- name: Clear Laravel Config
run: php artisan config:clear
php artisan optimize
- name: Execute Code Static Analysis with Larastan
run: ./vendor/bin/phpstan analyse -c ./phpstan.neon --no-progress --error-format=github
@ -63,12 +54,6 @@ jobs:
php-version: '8.3'
extensions: mbstring, dom, fileinfo
coverage: none
- name: Configure Laravel Nova Authentication
shell: bash
env:
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
run: composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
@ -84,10 +69,7 @@ jobs:
run: |
php -r "file_exists('.env') || copy('.env.ci', '.env');"
php artisan key:generate
php artisan config:cache
php artisan route:cache
- name: Clear Laravel Config
run: php artisan config:clear
php artisan optimize
- name: Run Pint Code Style Fixer
run: ./vendor/bin/pint
- uses: stefanzweifel/git-auto-commit-action@v5

View File

@ -25,12 +25,6 @@ jobs:
php-version: '8.3'
extensions: mbstring, dom, fileinfo
coverage: none
- name: Configure Laravel Nova Authentication
shell: bash
env:
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
run: composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
@ -59,8 +53,7 @@ jobs:
run: |
php -r "file_exists('.env') || copy('.env.ci', '.env');"
php artisan key:generate
php artisan config:cache
php artisan route:cache
php artisan optimize
- name: Run Database Migrations
run: php artisan migrate
- name: Link Storage

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
config/psysh

View File

@ -0,0 +1,91 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->maxLength(255),
Forms\Components\DateTimePicker::make('email_verified_at'),
Forms\Components\TextInput::make('password')
->password()
->required()
->maxLength(255),
Forms\Components\TextInput::make('user_role_id')
->numeric(),
Forms\Components\TextInput::make('profile_photo_path')
->maxLength(2048),
Forms\Components\DateTimePicker::make('created_at'),
Forms\Components\DateTimePicker::make('updated_at'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('role.name')
->sortable(),
Tables\Columns\TextColumn::make('email_verified_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\LoginUserRequest;
use App\Models\User;
use App\Traits\ApiResponses;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
use ApiResponses;
public function login(LoginUserRequest $request): JsonResponse
{
$request->validated($request->all());
if (! Auth::attempt($request->only('email', 'password'))) {
return $this->error(__('Invalid credentials'), 401);
}
$user = User::firstWhere('email', $request->email);
$tokenName = $request->token_name ?? __('Dynamic API Token');
return $this->success(__('Authenticated'), [
// Only allowing the 'read' scope to be dynamically created. Can revisit later when writes are possible.
'token' => $user->createToken($tokenName, ['read'])->plainTextToken,
]);
}
public function logout(Request $request): JsonResponse
{
/** @var \Laravel\Sanctum\PersonalAccessToken $token */
$token = $request->user()->currentAccessToken();
$token->delete();
return $this->success(__('Revoked API token'));
}
public function logoutAll(Request $request): JsonResponse
{
$request->user()->tokens()->delete();
return $this->success(__('Revoked all API tokens'));
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api\V0;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V0\StoreModRequest;
use App\Http\Requests\Api\V0\UpdateModRequest;
use App\Http\Resources\Api\V0\ModResource;
use App\Models\Mod;
class ModController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return ModResource::collection(Mod::paginate());
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreModRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Mod $mod)
{
return new ModResource($mod);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateModRequest $request, Mod $mod)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Mod $mod)
{
//
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api\V0;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V0\StoreUserRequest;
use App\Http\Requests\Api\V0\UpdateUserRequest;
use App\Http\Resources\Api\V0\UserResource;
use App\Models\User;
class UsersController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return UserResource::collection(User::paginate());
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreUserRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(User $user)
{
return new UserResource($user);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateUserRequest $request, User $user)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(User $user)
{
//
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class LoginUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'token_name' => ['sometimes'],
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Api\V0;
use Illuminate\Foundation\Http\FormRequest;
class StoreModRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Api\V0;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Api\V0;
use Illuminate\Foundation\Http\FormRequest;
class UpdateModRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Api\V0;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Resources\Api\V0;
use App\Models\Mod;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Mod */
class ModResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'type' => 'mod',
'id' => $this->id,
'attributes' => [
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->when(
$request->routeIs('api.v0.mods.show'),
$this->description
),
'source_code_link' => $this->source_code_link,
'user_id' => $this->user_id,
'license_id' => $this->license_id,
'created_at' => $this->created_at,
],
'relationships' => [
'user' => [
'data' => [
'type' => 'user',
'id' => $this->user_id,
],
// TODO: Provide 'links.self' to user profile:
//'links' => ['self' => '#'],
],
'license' => [
'data' => [
'type' => 'license',
'id' => $this->license_id,
],
],
],
'included' => [
new UserResource($this->user),
// TODO: Provide 'included' data for attached 'license':
//new LicenseResource($this->license),
],
'links' => [
'self' => route('mod.show', [
'mod' => $this->id,
'slug' => $this->slug,
]),
],
];
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Resources\Api\V0;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin User */
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'type' => 'user',
'id' => $this->id,
'attributes' => [
'name' => $this->name,
'user_role_id' => $this->user_role_id,
'created_at' => $this->created_at,
],
'relationships' => [
'user_role' => [
'data' => [
'type' => 'user_role',
'id' => $this->user_role_id,
],
],
],
// TODO: Provide 'included' data for attached 'user_role'
//'included' => [new UserRoleResource($this->role)],
// TODO: Provide 'links.self' to user profile:
//'links' => ['self' => '#'],
];
}
}

View File

@ -3,19 +3,74 @@
namespace App\Livewire;
use App\Models\Mod;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Livewire\Component;
class GlobalSearch extends Component
{
/**
* The search query.
*/
public string $query = '';
/**
* Whether to show the search result dropdown.
*/
public bool $showDropdown = false;
/**
* Whether to show the "no results found" message.
*/
public bool $noResults = false;
public function render(): View
{
$results = $this->query ? Mod::search($this->query)->get() : collect();
return view('livewire.global-search', [
'results' => $results,
'results' => $this->executeSearch($this->query),
]);
}
/**
* Execute the search against each of the searchable models.
*/
protected function executeSearch(string $query): array
{
$query = Str::trim($query);
$results = ['data' => [], 'total' => 0];
if (Str::length($query)) {
$results['data'] = [
'user' => User::search($query)->get(),
'mod' => Mod::search($query)->get(),
];
$results['total'] = $this->countTotalResults($results['data']);
}
$this->showDropdown = Str::length($query) > 0;
$this->noResults = $results['total'] === 0 && $this->showDropdown;
return $results;
}
/**
* Count the total number of results across all models.
*/
protected function countTotalResults($results): int
{
return collect($results)->reduce(function ($carry, $result) {
return $carry + $result->count();
}, 0);
}
/**
* Clear the search query and hide the dropdown.
*/
public function clearSearch(): void
{
$this->query = '';
$this->showDropdown = false;
$this->noResults = false;
}
}

View File

@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Scout\Searchable;
@ -31,17 +32,10 @@ class Mod extends Model
protected static function booted(): void
{
// Apply the global scope to exclude disabled mods.
static::addGlobalScope(new DisabledScope);
}
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);
@ -57,6 +51,9 @@ class Mod extends Model
return $this->hasMany(ModVersion::class);
}
/**
* Scope a query to include the total number of downloads for a mod.
*/
public function scopeWithTotalDownloads($query)
{
return $query->addSelect(['total_downloads' => ModVersion::selectRaw('SUM(downloads) AS total_downloads')
@ -110,6 +107,9 @@ class Mod extends Model
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion']);
}
/**
* Get the indexable data array for the model.
*/
public function toSearchableArray(): array
{
return [
@ -124,8 +124,45 @@ class Mod extends Model
];
}
/**
* Determine if the model should be searchable.
*/
public function shouldBeSearchable(): bool
{
return ! $this->disabled;
}
/**
* Get the URL to the thumbnail.
*/
public function thumbnailUrl(): Attribute
{
return Attribute::get(function (): string {
return $this->thumbnail
? Storage::disk($this->thumbnailDisk())->url($this->thumbnail)
: '';
});
}
/**
* Get the disk where the thumbnail is stored.
*/
protected function thumbnailDisk(): string
{
return match (config('app.env')) {
'production' => 'r2', // Cloudflare R2 Storage
default => 'public', // Local
};
}
/**
* Ensure the slug is always lower case when retrieved and slugified when saved.
*/
protected function slug(): Attribute
{
return Attribute::make(
get: fn (string $value) => Str::lower($value),
set: fn (string $value) => Str::slug($value),
);
}
}

View File

@ -72,9 +72,14 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->belongsTo(UserRole::class, 'user_role_id');
}
public function isMod(): bool
{
return Str::lower($this->role?->name) === 'moderator';
}
public function isAdmin(): bool
{
return Str::lower($this->role->name) === 'administrator';
return Str::lower($this->role?->name) === 'administrator';
}
protected function casts(): array
@ -84,4 +89,15 @@ class User extends Authenticatable implements MustVerifyEmail
'password' => 'hashed',
];
}
/**
* Get the disk that profile photos should be stored on.
*/
protected function profilePhotoDisk(): string
{
return match (config('app.env')) {
'production' => 'r2', // Cloudflare R2 Storage
default => 'public', // Local
};
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Nova\Dashboards;
use Laravel\Nova\Cards\Help;
use Laravel\Nova\Dashboards\Main as Dashboard;
class Main extends Dashboard
{
/**
* Get the cards for the dashboard.
*
* @return array
*/
public function cards()
{
return [
new Help,
];
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
class License extends Resource
{
public static $model = \App\Models\License::class;
public static $title = 'name';
public static $search = [
'id', 'name', 'link',
];
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
Text::make('Name')
->sortable()
->rules('required'),
Text::make('Link')
->sortable()
->rules('required'),
HasMany::make('Mods'),
];
}
public function cards(Request $request): array
{
return [];
}
public function filters(Request $request): array
{
return [];
}
public function lenses(Request $request): array
{
return [];
}
public function actions(Request $request): array
{
return [];
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
class Mod extends Resource
{
public static $model = \App\Models\Mod::class;
public static $title = 'name';
public static $search = [
'id', 'name', 'slug', 'description', 'source_code_link',
];
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
Text::make('Name')
->sortable()
->rules('required'),
Text::make('Slug')
->sortable()
->rules('required'),
Text::make('Description')
->sortable()
->rules('required'),
Text::make('Source Code Link')
->sortable()
->rules('required'),
Boolean::make('Contains AI Content')
->sortable()
->rules('required'),
BelongsTo::make('User', 'user', User::class),
BelongsTo::make('License', 'license', License::class),
HasMany::make('Versions', 'versions', ModVersion::class),
];
}
public function cards(Request $request): array
{
return [];
}
public function filters(Request $request): array
{
return [];
}
public function lenses(Request $request): array
{
return [];
}
public function actions(Request $request): array
{
return [];
}
}

View File

@ -1,72 +0,0 @@
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
class ModVersion extends Resource
{
public static $model = \App\Models\ModVersion::class;
public static $title = 'id';
public static $search = [
'id', 'version', 'description', 'virus_total_link',
];
public static function label(): string
{
return 'Mod Versions';
}
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
Text::make('Version')
->sortable()
->rules('required'),
Text::make('Description')
->sortable()
->rules('required'),
Text::make('Virus Total Link')
->sortable()
->rules('required'),
Number::make('Downloads')
->sortable()
->rules('required', 'integer'),
BelongsTo::make('Mod', 'mod', Mod::class),
BelongsTo::make('SptVersion', 'spt_version', SptVersion::class),
];
}
public function cards(Request $request): array
{
return [];
}
public function filters(Request $request): array
{
return [];
}
public function lenses(Request $request): array
{
return [];
}
public function actions(Request $request): array
{
return [];
}
}

View File

@ -1,55 +0,0 @@
<?php
namespace App\Nova;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Resource as NovaResource;
abstract class Resource extends NovaResource
{
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
return $query;
}
/**
* Build a Scout search query for the given resource.
*
* @param \Laravel\Scout\Builder $query
* @return \Laravel\Scout\Builder
*/
public static function scoutQuery(NovaRequest $request, $query)
{
return $query;
}
/**
* Build a "detail" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function detailQuery(NovaRequest $request, $query)
{
return parent::detailQuery($request, $query);
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableQuery(NovaRequest $request, $query)
{
return parent::relatableQuery($request, $query);
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
class SptVersion extends Resource
{
public static $model = \App\Models\SptVersion::class;
public static $title = 'id';
public static $search = [
'id', 'version', 'color_class',
];
public static function label(): string
{
return 'SPT Versions';
}
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
Text::make('Version')
->sortable()
->rules('required'),
Text::make('Color Class')
->sortable()
->rules('required'),
];
}
public function cards(Request $request): array
{
return [];
}
public function filters(Request $request): array
{
return [];
}
public function lenses(Request $request): array
{
return [];
}
public function actions(Request $request): array
{
return [];
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Nova;
use Illuminate\Validation\Rules;
use Laravel\Nova\Fields\Gravatar;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Password;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class User extends Resource
{
/**
* The model the resource corresponds to.
*/
public static $model = \App\Models\User::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id', 'name', 'email',
];
/**
* Get the fields displayed by the resource.
*
* @return array
*/
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Gravatar::make()->maxWidth(50),
Text::make('Name')
->sortable()
->rules('required', 'max:255'),
Text::make('Email')
->sortable()
->rules('required', 'email', 'max:254')
->creationRules('unique:users,email')
->updateRules('unique:users,email,{{resourceId}}'),
Password::make('Password')
->onlyOnForms()
->creationRules('required', Rules\Password::defaults())
->updateRules('nullable', Rules\Password::defaults()),
HasMany::make('Mods', 'mods', Mod::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Nova;
use App\Models\UserRole;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
class UserRoleResource extends Resource
{
public static string $model = UserRole::class;
public static $title = 'name';
public static $search = [
'id', 'name', 'description',
];
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
Text::make('Name')
->sortable()
->rules('required'),
Text::make('Description')
->sortable()
->rules('required'),
];
}
public function cards(Request $request): array
{
return [];
}
public function filters(Request $request): array
{
return [];
}
public function lenses(Request $request): array
{
return [];
}
public function actions(Request $request): array
{
return [];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->colors([
'primary' => Color::Blue,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}

View File

@ -1,81 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Nova\Nova;
use Laravel\Nova\NovaApplicationServiceProvider;
class NovaServiceProvider extends NovaApplicationServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
parent::boot();
Nova::withBreadcrumbs();
}
/**
* Get the tools that should be listed in the Nova sidebar.
*
* @return array
*/
public function tools()
{
return [];
}
/**
* Register the Nova routes.
*
* @return void
*/
protected function routes()
{
Nova::routes()
->withAuthenticationRoutes()
->withPasswordResetRoutes()
->register();
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Register the Nova gate.
*
* This gate determines who can access Nova in non-local environments.
*
* @return void
*/
protected function gate()
{
Gate::define('viewNova', function ($user) {
return $user->isAdmin();
});
}
/**
* Get the dashboards that should be listed in the Nova sidebar.
*
* @return array
*/
protected function dashboards()
{
return [
new \App\Nova\Dashboards\Main,
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Traits;
use Illuminate\Http\JsonResponse;
trait ApiResponses
{
protected function success(string $message, ?array $data = []): JsonResponse
{
return $this->baseResponse(message: $message, data: $data, code: 200);
}
private function baseResponse(?string $message = '', ?array $data = [], ?int $code = 200): JsonResponse
{
return response()->json([
'message' => $message,
'data' => $data,
], $code);
}
protected function error(string $message, int $code): JsonResponse
{
return $this->baseResponse(message: $message, code: $code);
}
}

View File

@ -3,6 +3,7 @@
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Route;
use Mchev\Banhammer\Middleware\IPBanned;
return Application::configure(basePath: dirname(__DIR__))
@ -11,6 +12,12 @@ return Application::configure(basePath: dirname(__DIR__))
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware('api')
->prefix('api/v0')
->name('api.v0.')
->group(base_path('routes/api_v0.php'));
},
)
->withMiddleware(function (Middleware $middleware) {
$middleware->append(IPBanned::class);

View File

@ -5,5 +5,5 @@ return [
App\Providers\FortifyServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\JetstreamServiceProvider::class,
App\Providers\NovaServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
];

View File

@ -8,11 +8,11 @@
"php": "^8.3",
"ext-curl": "*",
"aws/aws-sdk-php": "^3.314",
"filament/filament": "^3.2",
"http-interop/http-factory-guzzle": "^1.2",
"laravel/framework": "^11.11",
"laravel/horizon": "^5.24",
"laravel/jetstream": "^5.1",
"laravel/nova": "^4.34",
"laravel/octane": "^2.4",
"laravel/pulse": "^1.2",
"laravel/sanctum": "^4.0",
@ -49,9 +49,13 @@
}
},
"scripts": {
"phpstan": [
"./vendor/bin/phpstan analyse --debug --memory-limit=2G"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
"@php artisan package:discover --ansi",
"@php artisan filament:upgrade"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
@ -66,12 +70,6 @@
"@php artisan migrate --graceful --ansi"
]
},
"repositories": [
{
"type": "composer",
"url": "https://nova.laravel.com"
}
],
"extra": {
"laravel": {
"dont-discover": []

2541
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ return [
|
*/
'use' => 'default',
'use' => 'queue',
/*
|--------------------------------------------------------------------------

View File

@ -114,7 +114,7 @@ return [
|
*/
'inject_assets' => false,
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
@ -157,16 +157,4 @@ return [
*/
'pagination_theme' => 'tailwind',
/*
|---------------------------------------------------------------------------
| Asset URL
|---------------------------------------------------------------------------
|
| When serving assets from a CDN, you may need to override the asset URL
| that Livewire uses to include assets in the response.
|
*/
'asset_url' => env('ASSET_URL_LIVEWIRE'),
];

View File

@ -1,206 +0,0 @@
<?php
use Laravel\Nova\Actions\ActionResource;
use Laravel\Nova\Http\Middleware\Authenticate;
use Laravel\Nova\Http\Middleware\Authorize;
use Laravel\Nova\Http\Middleware\BootTools;
use Laravel\Nova\Http\Middleware\DispatchServingNovaEvent;
use Laravel\Nova\Http\Middleware\HandleInertiaRequests;
return [
/*
|--------------------------------------------------------------------------
| Nova License Key
|--------------------------------------------------------------------------
|
| The following configuration option contains your Nova license key. On
| non-local domains, Nova will verify that the Nova installation has
| a valid license associated with the application's active domain.
|
*/
'license_key' => env('NOVA_LICENSE_KEY'),
/*
|--------------------------------------------------------------------------
| Nova App Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to display the name of the application within the UI
| or in other locations. Of course, you're free to change the value.
|
*/
'name' => env('NOVA_APP_NAME', env('APP_NAME')),
/*
|--------------------------------------------------------------------------
| Nova Domain Name
|--------------------------------------------------------------------------
|
| This value is the "domain name" associated with your application. This
| can be used to prevent Nova's internal routes from being registered
| on subdomains which do not need access to your admin application.
|
*/
'domain' => env('NOVA_DOMAIN_NAME', null),
/*
|--------------------------------------------------------------------------
| Nova Path
|--------------------------------------------------------------------------
|
| This is the URI path where Nova will be accessible from. Feel free to
| change this path to anything you like. Note that this URI will not
| affect Nova's internal API routes which aren't exposed to users.
|
*/
'path' => '/nova',
/*
|--------------------------------------------------------------------------
| Nova Authentication Guard
|--------------------------------------------------------------------------
|
| This configuration option defines the authentication guard that will
| be used to protect your Nova routes. This option should match one
| of the authentication guards defined in the "auth" config file.
|
*/
'guard' => env('NOVA_GUARD', null),
/*
|--------------------------------------------------------------------------
| Nova Password Reset Broker
|--------------------------------------------------------------------------
|
| This configuration option defines the password broker that will be
| used when passwords are reset. This option should mirror one of
| the password reset options defined in the "auth" config file.
|
*/
'passwords' => env('NOVA_PASSWORDS', null),
/*
|--------------------------------------------------------------------------
| Nova Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will be assigned to every Nova route, giving you the
| chance to add your own middleware to this stack or override any of
| the existing middleware. Or, you can just stick with this stack.
|
*/
'middleware' => [
'web',
HandleInertiaRequests::class,
DispatchServingNovaEvent::class,
BootTools::class,
],
'api_middleware' => [
'nova',
Authenticate::class,
Authorize::class,
],
/*
|--------------------------------------------------------------------------
| Nova Pagination Type
|--------------------------------------------------------------------------
|
| This option defines the visual style used in Nova's resource pagination
| views. You may select between "simple", "load-more", and "links" for
| your applications. Feel free to adjust this option to your choice.
|
*/
'pagination' => 'simple',
/*
|--------------------------------------------------------------------------
| Nova Storage Disk
|--------------------------------------------------------------------------
|
| This configuration option allows you to define the default disk that
| will be used to store files using the Image, File, and other file
| related field types. You're welcome to use any configured disk.
|
*/
'storage_disk' => env('NOVA_STORAGE_DISK', 'public'),
/*
|--------------------------------------------------------------------------
| Nova Currency
|--------------------------------------------------------------------------
|
| This configuration option allows you to define the default currency
| used by the Currency field within Nova. You may change this to a
| valid ISO 4217 currency code to suit your application's needs.
|
*/
'currency' => 'USD',
/*
|--------------------------------------------------------------------------
| Branding
|--------------------------------------------------------------------------
|
| These configuration values allow you to customize the branding of the
| Nova interface, including the primary color and the logo that will
| be displayed within the Nova interface. This logo value must be
| the absolute path to an SVG logo within the local filesystem.
|
*/
// 'brand' => [
// 'logo' => resource_path('/img/example-logo.svg'),
// 'colors' => [
// "400" => "24, 182, 155, 0.5",
// "500" => "24, 182, 155",
// "600" => "24, 182, 155, 0.75",
// ]
// ],
/*
|--------------------------------------------------------------------------
| Nova Action Resource Class
|--------------------------------------------------------------------------
|
| This configuration option allows you to specify a custom resource class
| to use for action log entries instead of the default that ships with
| Nova, thus allowing for the addition of additional UI form fields.
|
*/
'actions' => [
'resource' => ActionResource::class,
],
/*
|--------------------------------------------------------------------------
| Nova Impersonation Redirection URLs
|--------------------------------------------------------------------------
|
| This configuration option allows you to specify a URL where Nova should
| redirect an administrator after impersonating another user and a URL
| to redirect the administrator after stopping impersonating a user.
|
*/
'impersonation' => [
'started' => '/',
'stopped' => '/',
],
];

View File

@ -180,9 +180,11 @@ return [
'bootstrap',
'config/**/*.php',
'database/**/*.php',
'lang/**/*.php',
'public/**/*.php',
'resources/**/*.php',
'routes',
'tests/**/*.php',
'composer.lock',
'.env',
],

View File

@ -13,7 +13,7 @@ return [
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
'default' => env('QUEUE_CONNECTION', 'redis'),
/*
|--------------------------------------------------------------------------

View File

@ -19,7 +19,7 @@ return [
|
*/
'driver' => env('SCOUT_DRIVER', 'algolia'),
'driver' => env('SCOUT_DRIVER', 'meilisearch'),
/*
|--------------------------------------------------------------------------
@ -137,9 +137,13 @@ return [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
User::class => [
'filterableAttributes' => [],
'sortableAttributes' => [],
],
Mod::class => [
'filterableAttributes' => ['featured'],
'sortableAttributes' => ['created_at', 'updated_at'],
'filterableAttributes' => [],
'sortableAttributes' => [],
],
],
],

View File

@ -13,8 +13,8 @@ class LicenseFactory extends Factory
public function definition(): array
{
return [
'name' => $this->faker->name(),
'link' => $this->faker->url,
'name' => fake()->name(),
'link' => fake()->url(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];

View File

@ -7,27 +7,31 @@ use App\Models\Mod;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Random\RandomException;
class ModFactory extends Factory
{
protected $model = Mod::class;
/**
* @throws RandomException
*/
public function definition(): array
{
$name = $this->faker->words(3, true);
$name = fake()->catchPhrase();
return [
'user_id' => User::factory(),
'name' => $name,
'slug' => Str::slug($name),
'teaser' => $this->faker->sentence,
'description' => $this->faker->sentences(6, true),
'teaser' => fake()->sentence(),
'description' => fake()->paragraphs(random_int(4, 20), true),
'license_id' => License::factory(),
'source_code_link' => $this->faker->url(),
'featured' => $this->faker->boolean,
'contains_ai_content' => $this->faker->boolean,
'contains_ads' => $this->faker->boolean,
'disabled' => $this->faker->boolean,
'source_code_link' => fake()->url(),
'featured' => fake()->boolean(),
'contains_ai_content' => fake()->boolean(),
'contains_ads' => fake()->boolean(),
'disabled' => fake()->boolean(),
'created_at' => now(),
'updated_at' => now(),
];

View File

@ -16,13 +16,13 @@ class ModVersionFactory extends Factory
{
return [
'mod_id' => Mod::factory(),
'version' => $this->faker->numerify('1.#.#'),
'description' => $this->faker->text(),
'link' => $this->faker->url(),
'version' => fake()->numerify('1.#.#'),
'description' => fake()->text(),
'link' => fake()->url(),
'spt_version_id' => SptVersion::factory(),
'virus_total_link' => $this->faker->url(),
'downloads' => $this->faker->randomNumber(),
'disabled' => $this->faker->boolean,
'virus_total_link' => fake()->url(),
'downloads' => fake()->randomNumber(),
'disabled' => fake()->boolean(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];

View File

@ -19,7 +19,7 @@ class UserFactory extends Factory
public function definition(): array
{
return [
'name' => fake()->name(),
'name' => fake()->userName(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),

View File

@ -14,12 +14,20 @@ return new class extends Migration
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->bigInteger('hub_id')
->nullable()
->default(null)
->unique();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->foreignIdFor(UserRole::class)->nullable()->default(null)->constrained('user_roles');
$table->foreignIdFor(UserRole::class)
->nullable()
->default(null)
->constrained('user_roles')
->nullOnDelete()
->cascadeOnUpdate();
$table->rememberToken();
$table->string('profile_photo_path', 2048)->nullable();
$table->timestamps();

View File

@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -1,57 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@ -12,14 +12,25 @@ return new class extends Migration
{
Schema::create('mods', function (Blueprint $table) {
$table->id();
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->foreignIdFor(User::class)->constrained('users');
$table->bigInteger('hub_id')
->nullable()
->default(null)
->unique();
$table->foreignIdFor(User::class)
->constrained('users')
->cascadeOnDelete()
->cascadeOnUpdate();
$table->string('name');
$table->string('slug');
$table->string('teaser');
$table->longText('description');
$table->string('thumbnail')->default('');
$table->foreignIdFor(License::class)->nullable()->default(null)->constrained('licenses');
$table->foreignIdFor(License::class)
->nullable()
->default(null)
->constrained('licenses')
->nullOnDelete()
->cascadeOnUpdate();
$table->string('source_code_link');
$table->boolean('featured')->default(false);
$table->boolean('contains_ai_content')->default(false);
@ -27,6 +38,8 @@ return new class extends Migration
$table->boolean('disabled')->default(false);
$table->softDeletes();
$table->timestamps();
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
});
}

View File

@ -10,11 +10,16 @@ return new class extends Migration
{
Schema::create('spt_versions', function (Blueprint $table) {
$table->id();
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->bigInteger('hub_id')
->nullable()
->default(null)
->unique();
$table->string('version');
$table->string('color_class');
$table->softDeletes();
$table->timestamps();
$table->index(['version', 'deleted_at'], 'spt_versions_filtering_index');
});
}

View File

@ -12,17 +12,30 @@ return new class extends Migration
{
Schema::create('mod_versions', function (Blueprint $table) {
$table->id();
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
$table->foreignIdFor(Mod::class)->constrained('mods');
$table->bigInteger('hub_id')
->nullable()
->default(null)
->unique();
$table->foreignIdFor(Mod::class)
->constrained('mods')
->cascadeOnDelete()
->cascadeOnUpdate();
$table->string('version');
$table->longText('description');
$table->string('link');
$table->foreignIdFor(SptVersion::class)->constrained('spt_versions');
$table->foreignIdFor(SptVersion::class)
->nullable()
->default(null)
->constrained('spt_versions')
->nullOnDelete()
->cascadeOnUpdate();
$table->string('virus_total_link');
$table->unsignedBigInteger('downloads');
$table->boolean('disabled')->default(false);
$table->softDeletes();
$table->timestamps();
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
});
}

View File

@ -1,38 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('mods', function (Blueprint $table) {
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
});
Schema::table('mod_versions', function (Blueprint $table) {
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
});
Schema::table('spt_versions', function (Blueprint $table) {
$table->index(['version', 'deleted_at'], 'spt_versions_filtering_index');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('mods', function (Blueprint $table) {
$table->dropIndex('mods_show_index');
$table->dropIndex('mod_versions_filtering_index');
$table->dropIndex('spt_versions_filtering_index');
});
}
};

View File

@ -25,7 +25,10 @@ class DatabaseSeeder extends Seeder
// Add 5 administrators.
$administrator = UserRole::factory()->administrator()->create();
User::factory(5)->for($administrator, 'role')->create();
User::factory()->for($administrator, 'role')->create([
'email' => 'test@example.com',
]);
User::factory(4)->for($administrator, 'role')->create();
// Add 10 moderators.
$moderator = UserRole::factory()->moderator()->create();

View File

@ -19,7 +19,7 @@ services:
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --host=localhost --port=443 --admin-port=2019 --https"
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --host=localhost --port=443 --admin-port=2019 --https --watch"
XDG_CONFIG_HOME: /var/www/html/config
XDG_DATA_HOME: /var/www/html/data
volumes:

135
package-lock.json generated
View File

@ -4,11 +4,15 @@
"requires": true,
"packages": {
"": {
"dependencies": {
"@alpinejs/focus": "^3.14.1"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.16",
"axios": "^1.6.4",
"chokidar": "^3.6.0",
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.32",
"prettier": "^3.2.5",
@ -30,6 +34,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@alpinejs/focus": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.14.1.tgz",
"integrity": "sha512-z4xdpK6X1LB2VitsWbL61tmABoOORuEhE5v2tnUX/be6/nAygXyeDxZ1x9s1u+bOEYlIOXXLmjdmTlhchUVWxw==",
"license": "MIT",
"dependencies": {
"focus-trap": "^6.9.4",
"tabbable": "^5.3.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -999,9 +1013,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001636",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
"integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
"version": "1.0.30001640",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz",
"integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==",
"dev": true,
"funding": [
{
@ -1044,19 +1058,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1160,9 +1161,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.4.807",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz",
"integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==",
"version": "1.4.816",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz",
"integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==",
"dev": true,
"license": "ISC"
},
@ -1239,19 +1240,6 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@ -1275,6 +1263,15 @@
"node": ">=8"
}
},
"node_modules/focus-trap": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz",
"integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==",
"license": "MIT",
"dependencies": {
"tabbable": "^5.3.3"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
@ -1392,16 +1389,16 @@
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
"is-glob": "^4.0.1"
},
"engines": {
"node": ">=10.13.0"
"node": ">= 6"
}
},
"node_modules/hasown": {
@ -1431,13 +1428,16 @@
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz",
"integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.0"
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -1581,9 +1581,9 @@
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz",
"integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==",
"dev": true,
"license": "ISC",
"engines": {
@ -1648,9 +1648,9 @@
}
},
"node_modules/minimatch": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
@ -1833,9 +1833,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true,
"funding": [
{
@ -1854,7 +1854,7 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
@ -2420,6 +2420,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tabbable": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==",
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
@ -2458,6 +2464,19 @@
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
@ -2516,9 +2535,9 @@
"license": "Apache-2.0"
},
"node_modules/update-browserslist-db": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
"integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"dev": true,
"funding": [
{
@ -2554,14 +2573,14 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
"integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.38",
"postcss": "^8.4.39",
"rollup": "^4.13.0"
},
"bin": {

View File

@ -10,11 +10,15 @@
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.16",
"axios": "^1.6.4",
"chokidar": "^3.6.0",
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.32",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.0",
"vite": "^5.0"
},
"dependencies": {
"@alpinejs/focus": "^3.14.1"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.fi-pagination-items,.fi-pagination-overview,.fi-pagination-records-per-page-select:not(.fi-compact){display:none}@supports (container-type:inline-size){.fi-pagination{container-type:inline-size}@container (min-width: 28rem){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@container (min-width: 56rem){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}@supports not (container-type:inline-size){@media (min-width:640px){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@media (min-width:768px){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{background-color:#333;border-radius:4px;color:#fff;font-size:14px;line-height:1.4;outline:0;position:relative;transition-property:transform,visibility,opacity;white-space:normal}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-top-color:initial;border-width:8px 8px 0;bottom:-7px;left:0;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:initial;border-width:0 8px 8px;left:0;top:-7px;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-left-color:initial;border-width:8px 0 8px 8px;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-right-color:initial;border-width:8px 8px 8px 0;left:-7px;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;height:16px;width:16px}.tippy-arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.tippy-content{padding:5px 9px;position:relative;z-index:1}.tippy-box[data-theme~=light]{background-color:#fff;box-shadow:0 0 20px 4px #9aa1b126,0 4px 80px -8px #24282f40,0 4px 4px -2px #5b5e6926;color:#26323d}.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff}.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff}.tippy-box[data-theme~=light]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=light]>.tippy-svg-arrow{fill:#fff}.fi-sortable-ghost{opacity:.3}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};

View File

@ -0,0 +1 @@
function i({initialHeight:t}){return{height:t+"rem",init:function(){this.setInitialHeight(),this.setUpResizeObserver()},setInitialHeight:function(){this.height=t+"rem",!(this.$el.scrollHeight<=0)&&(this.$el.style.height=this.height)},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.height!==e&&(this.height=e,this.$el.style.height=this.height)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.height=this.$el.style.height}).observe(this.$el)}}}export{i as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
function n(){return{collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){let e=this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[];for(let t of e)t.removeEventListener("click",this.handleCheckboxClick),t.addEventListener("click",s=>this.handleCheckboxClick(s,t))},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8660,7 +8660,7 @@ function extractDestinationFromLink(linkEl) {
return createUrlObjectFromString(linkEl.getAttribute("href"));
}
function createUrlObjectFromString(urlString) {
return new URL(urlString, document.baseURI);
return urlString !== null && new URL(urlString, document.baseURI);
}
function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash;
@ -9087,12 +9087,16 @@ function navigate_default(Alpine19) {
let shouldPrefetchOnHover = modifiers.includes("hover");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
});
whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});

View File

@ -7305,7 +7305,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return createUrlObjectFromString(linkEl.getAttribute("href"));
}
function createUrlObjectFromString(urlString) {
return new URL(urlString, document.baseURI);
return urlString !== null && new URL(urlString, document.baseURI);
}
function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash;
@ -7730,12 +7730,16 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
let shouldPrefetchOnHover = modifiers.includes("hover");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
});
whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
{"/livewire.js":"87e1046f"}
{"/livewire.js":"c4fc8c5d"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,46 +0,0 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
/*!
autosize 4.0.4
license: MIT
http://www.jacklmoore.com/autosize
*/
/*!
* @overview es6-promise - a tiny implementation of Promises/A+.
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
* @license Licensed under MIT license
* See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
* @version v4.2.8+1e68dce6
*/
/*!
* JavaScript Cookie v2.2.1
* https://github.com/js-cookie/js-cookie
*
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
* Released under the MIT license
*/
/*!
* vuex v4.1.0
* (c) 2022 Evan You
* @license MIT
*/
/*! Hammer.JS - v2.0.7 - 2016-04-22
* http://hammerjs.github.io/
*
* Copyright (c) 2016 Jorik Tangelder;
* Licensed under the MIT license */
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More