Initial commit
This commit is contained in:
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[{compose,docker-compose}.{yml,yaml}]
|
||||
indent_size = 4
|
||||
65
.env.example
Normal file
65
.env.example
Normal file
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mariadb
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=romhackplaza
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.codex
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/fonts-manifest.dev.json
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
|
||||
58
README.md
Normal file
58
README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||
|
||||
## Agentic Development
|
||||
|
||||
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||
|
||||
```bash
|
||||
composer require laravel/boost --dev
|
||||
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
77
app/Auth/XenForoGuard.php
Normal file
77
app/Auth/XenForoGuard.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Auth;
|
||||
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class XenForoGuard implements Guard
|
||||
{
|
||||
private ?XenForoUser $user = null;
|
||||
|
||||
public function __construct(private readonly Request $request) {}
|
||||
|
||||
public function check(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function guest(): bool
|
||||
{
|
||||
return ! $this->check();
|
||||
}
|
||||
|
||||
public function id(): mixed
|
||||
{
|
||||
return $this->user()?->getAuthIdentifier();
|
||||
}
|
||||
|
||||
public function hasUser(): bool
|
||||
{
|
||||
return $this->user !== null;
|
||||
}
|
||||
|
||||
public function user(): ?XenForoUser
|
||||
{
|
||||
if ($this->hasUser())
|
||||
return $this->user;
|
||||
|
||||
$sessionId = $this->request->cookie('xf_session');
|
||||
if(!$sessionId)
|
||||
return null;
|
||||
|
||||
$xfSession = \DB::connection('xenforo')
|
||||
->table('session')
|
||||
->where('session_id', $sessionId)
|
||||
->value('session_data');
|
||||
|
||||
if(!$xfSession)
|
||||
return null;
|
||||
|
||||
$sessionData = unserialize($xfSession);
|
||||
|
||||
if (!$sessionData || !isset($sessionData['userId']) || !$sessionData['userId'])
|
||||
return null;
|
||||
|
||||
$xfUser = \DB::connection('xenforo')
|
||||
->table('user')
|
||||
->where('user_id', $sessionData['userId'])
|
||||
->first();
|
||||
|
||||
if(!$xfUser)
|
||||
return null;
|
||||
|
||||
return $this->user = new XenForoUser($xfUser);
|
||||
}
|
||||
|
||||
public function validate(array $credentials = []): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setUser(mixed $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
}
|
||||
84
app/Auth/XenForoUser.php
Normal file
84
app/Auth/XenForoUser.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Auth;
|
||||
|
||||
use App\Services\XenforoService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class XenForoUser implements Authenticatable {
|
||||
|
||||
public ?array $permissions = null;
|
||||
private XenforoService $services;
|
||||
|
||||
public function __construct(public readonly object $data) {
|
||||
$this->services = app(XenforoService::class);
|
||||
}
|
||||
|
||||
public function __get(string $name): mixed {
|
||||
return $this->data->$name ?? null;
|
||||
}
|
||||
|
||||
public function getAuthIdentifierName(): string
|
||||
{
|
||||
return 'user_id';
|
||||
}
|
||||
|
||||
public function getAuthIdentifier(): mixed
|
||||
{
|
||||
return $this->data->user_id;
|
||||
}
|
||||
|
||||
public function getAuthPasswordName(): string
|
||||
{
|
||||
return 'password';
|
||||
}
|
||||
|
||||
public function getAuthPassword(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getRememberToken(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function setRememberToken($value): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public function getRememberTokenName(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get XenForo avatar if it exist.
|
||||
*
|
||||
* @param string $xfSize
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAvatarUrl( string $xfSize = 'm' ): ?string
|
||||
{
|
||||
$userId = $this->data->user_id;
|
||||
$avatarDate = $this->data->avatar_date;
|
||||
|
||||
if( $avatarDate ){
|
||||
$group = floor($userId / 1000);
|
||||
return config('app.forum_url') . "/data/avatars/{$xfSize}/{$group}/{$userId}.jpg?{$avatarDate}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function can(string $permissionGroup, string $permissionName): bool
|
||||
{
|
||||
if( !$this->permissions ){
|
||||
$this->permissions = $this->services->getPermissions($this->data->user_id, $this->data->permission_combination_id);
|
||||
}
|
||||
|
||||
return ($this->permissions[$permissionGroup][$permissionName] ?? 0) === true;
|
||||
}
|
||||
}
|
||||
10
app/Exceptions/SubmissionException.php
Normal file
10
app/Exceptions/SubmissionException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class SubmissionException extends Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
50
app/Filament/Resources/Authors/AuthorResource.php
Normal file
50
app/Filament/Resources/Authors/AuthorResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Authors;
|
||||
|
||||
use App\Filament\Resources\Authors\Pages\CreateAuthor;
|
||||
use App\Filament\Resources\Authors\Pages\EditAuthor;
|
||||
use App\Filament\Resources\Authors\Pages\ListAuthors;
|
||||
use App\Filament\Resources\Authors\Schemas\AuthorForm;
|
||||
use App\Filament\Resources\Authors\Tables\AuthorsTable;
|
||||
use App\Models\Author;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AuthorResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Author::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::UserGroup;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return AuthorForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return AuthorsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAuthors::route('/'),
|
||||
'create' => CreateAuthor::route('/create'),
|
||||
'edit' => EditAuthor::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Authors/Pages/CreateAuthor.php
Normal file
11
app/Filament/Resources/Authors/Pages/CreateAuthor.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Authors\Pages;
|
||||
|
||||
use App\Filament\Resources\Authors\AuthorResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAuthor extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AuthorResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Authors/Pages/EditAuthor.php
Normal file
19
app/Filament/Resources/Authors/Pages/EditAuthor.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Authors\Pages;
|
||||
|
||||
use App\Filament\Resources\Authors\AuthorResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditAuthor extends EditRecord
|
||||
{
|
||||
protected static string $resource = AuthorResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Authors/Pages/ListAuthors.php
Normal file
19
app/Filament/Resources/Authors/Pages/ListAuthors.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Authors\Pages;
|
||||
|
||||
use App\Filament\Resources\Authors\AuthorResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAuthors extends ListRecords
|
||||
{
|
||||
protected static string $resource = AuthorResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Filament/Resources/Authors/Schemas/AuthorForm.php
Normal file
30
app/Filament/Resources/Authors/Schemas/AuthorForm.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Authors\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class AuthorForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
TextInput::make('slug')
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
TextInput::make('website')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->default(null),
|
||||
TextInput::make('user_id')
|
||||
->numeric()
|
||||
->default(null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
47
app/Filament/Resources/Authors/Tables/AuthorsTable.php
Normal file
47
app/Filament/Resources/Authors/Tables/AuthorsTable.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Authors\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AuthorsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('website')
|
||||
->searchable(),
|
||||
TextColumn::make('user_id')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/Entries/EntryResource.php
Normal file
50
app/Filament/Resources/Entries/EntryResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries;
|
||||
|
||||
use App\Filament\Resources\Entries\Pages\CreateEntry;
|
||||
use App\Filament\Resources\Entries\Pages\EditEntry;
|
||||
use App\Filament\Resources\Entries\Pages\ListEntries;
|
||||
use App\Filament\Resources\Entries\Schemas\EntryForm;
|
||||
use App\Filament\Resources\Entries\Tables\EntriesTable;
|
||||
use App\Models\Entry;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EntryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Entry::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return EntryForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return EntriesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListEntries::route('/'),
|
||||
'create' => CreateEntry::route('/create'),
|
||||
'edit' => EditEntry::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Entries/Pages/CreateEntry.php
Normal file
11
app/Filament/Resources/Entries/Pages/CreateEntry.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateEntry extends CreateRecord
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Entries/Pages/EditEntry.php
Normal file
19
app/Filament/Resources/Entries/Pages/EditEntry.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditEntry extends EditRecord
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Entries/Pages/ListEntries.php
Normal file
19
app/Filament/Resources/Entries/Pages/ListEntries.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListEntries extends ListRecords
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
139
app/Filament/Resources/Entries/Schemas/EntryForm.php
Normal file
139
app/Filament/Resources/Entries/Schemas/EntryForm.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Schemas;
|
||||
|
||||
use App\Models\Author;
|
||||
use App\Models\Game;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class EntryForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('type')
|
||||
->options([
|
||||
'translations' => 'Translations',
|
||||
'romhacks' => 'Romhacks',
|
||||
'homebrew' => 'Homebrew',
|
||||
'utilities' => 'Utilities',
|
||||
'documents' => 'Documents',
|
||||
'lua-scripts' => 'Lua scripts',
|
||||
'tutorials' => 'Tutorials',
|
||||
])
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('title')
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
TextInput::make('slug')
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
Textarea::make('description')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
FileUpload::make('main_image')
|
||||
->image(),
|
||||
Select::make('state')
|
||||
->options([
|
||||
'draft' => 'Draft',
|
||||
'pending' => 'Pending',
|
||||
'published' => 'Published',
|
||||
'locked' => 'Locked',
|
||||
'hidden' => 'Hidden',
|
||||
])
|
||||
->default('draft')
|
||||
->required(),
|
||||
Toggle::make('featured')
|
||||
->required(),
|
||||
Select::make('game_id')
|
||||
->relationship('game', 'name')
|
||||
->searchable()
|
||||
->default(null)
|
||||
->hidden(fn(Get $get) => $get('type') === 'homebrew')
|
||||
->createOptionForm([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('slug')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Select::make('platform_id')
|
||||
->relationship('platform', 'name')
|
||||
->required(),
|
||||
Select::make('genre_id')
|
||||
->relationship('genre', 'name')
|
||||
->required(),
|
||||
])
|
||||
->createOptionUsing(function (array $data){
|
||||
return Game::create( $data )->id;
|
||||
})
|
||||
,
|
||||
Select::make('platform_id')
|
||||
->relationship('platform', 'name')
|
||||
->default(null)
|
||||
->hidden(fn(Get $get) => in_array( $get('type'), [ 'translations', 'romhacks' ] ) ),
|
||||
Select::make('status_id')
|
||||
->relationship('status', 'id')
|
||||
->default(null),
|
||||
Select::make('authors')
|
||||
->relationship('authors', 'name')
|
||||
->searchable()
|
||||
->multiple()
|
||||
->createOptionForm([
|
||||
TextInput::make('name')
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
TextInput::make('slug')
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
TextInput::make('website')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->default(null),
|
||||
TextInput::make('user_id')
|
||||
->numeric()
|
||||
->default(null),
|
||||
])
|
||||
->createOptionUsing(function (array $data){
|
||||
return Author::create( $data )->id;
|
||||
})
|
||||
,
|
||||
Select::make('languages')
|
||||
->relationship('languages', 'name')
|
||||
->multiple()
|
||||
->preload(),
|
||||
Select::make('modifications')
|
||||
->relationship('modifications', 'name')
|
||||
->multiple()
|
||||
->preload()
|
||||
->hidden(fn(Get $get) => $get('type') !== "romhacks"),
|
||||
TextInput::make('version')
|
||||
->default(null),
|
||||
DatePicker::make('release_date'),
|
||||
Textarea::make('staff_credits')
|
||||
->default(null)
|
||||
->columnSpanFull(),
|
||||
TextInput::make('relevant_link')
|
||||
->default(null),
|
||||
TextInput::make('youtube_link')
|
||||
->default(null),
|
||||
TextInput::make('user_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
TextInput::make('comments_thread_id')
|
||||
->numeric()
|
||||
->default(null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
app/Filament/Resources/Entries/Tables/EntriesTable.php
Normal file
58
app/Filament/Resources/Entries/Tables/EntriesTable.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EntriesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('type')
|
||||
->badge(),
|
||||
TextColumn::make('title')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('state')
|
||||
->badge(),
|
||||
IconColumn::make('featured')
|
||||
->boolean(),
|
||||
TextColumn::make('version')
|
||||
->searchable(),
|
||||
TextColumn::make('user_id')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('comments_thread_id')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/Games/GameResource.php
Normal file
50
app/Filament/Resources/Games/GameResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Games;
|
||||
|
||||
use App\Filament\Resources\Games\Pages\CreateGame;
|
||||
use App\Filament\Resources\Games\Pages\EditGame;
|
||||
use App\Filament\Resources\Games\Pages\ListGames;
|
||||
use App\Filament\Resources\Games\Schemas\GameForm;
|
||||
use App\Filament\Resources\Games\Tables\GamesTable;
|
||||
use App\Models\Game;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class GameResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Game::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::PlayCircle;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return GameForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return GamesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListGames::route('/'),
|
||||
'create' => CreateGame::route('/create'),
|
||||
'edit' => EditGame::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Games/Pages/CreateGame.php
Normal file
11
app/Filament/Resources/Games/Pages/CreateGame.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Games\Pages;
|
||||
|
||||
use App\Filament\Resources\Games\GameResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateGame extends CreateRecord
|
||||
{
|
||||
protected static string $resource = GameResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Games/Pages/EditGame.php
Normal file
19
app/Filament/Resources/Games/Pages/EditGame.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Games\Pages;
|
||||
|
||||
use App\Filament\Resources\Games\GameResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditGame extends EditRecord
|
||||
{
|
||||
protected static string $resource = GameResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Games/Pages/ListGames.php
Normal file
19
app/Filament/Resources/Games/Pages/ListGames.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Games\Pages;
|
||||
|
||||
use App\Filament\Resources\Games\GameResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListGames extends ListRecords
|
||||
{
|
||||
protected static string $resource = GameResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Filament/Resources/Games/Schemas/GameForm.php
Normal file
29
app/Filament/Resources/Games/Schemas/GameForm.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Games\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class GameForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('slug')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Select::make('platform_id')
|
||||
->relationship('platform', 'name')
|
||||
->required(),
|
||||
Select::make('genre_id')
|
||||
->relationship('genre', 'name')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
46
app/Filament/Resources/Games/Tables/GamesTable.php
Normal file
46
app/Filament/Resources/Games/Tables/GamesTable.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Games\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class GamesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('platform.name')
|
||||
->searchable(),
|
||||
TextColumn::make('genre.name')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/Genres/GenreResource.php
Normal file
50
app/Filament/Resources/Genres/GenreResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Genres;
|
||||
|
||||
use App\Filament\Resources\Genres\Pages\CreateGenre;
|
||||
use App\Filament\Resources\Genres\Pages\EditGenre;
|
||||
use App\Filament\Resources\Genres\Pages\ListGenres;
|
||||
use App\Filament\Resources\Genres\Schemas\GenreForm;
|
||||
use App\Filament\Resources\Genres\Tables\GenresTable;
|
||||
use App\Models\Genre;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class GenreResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Genre::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::FingerPrint;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return GenreForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return GenresTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListGenres::route('/'),
|
||||
'create' => CreateGenre::route('/create'),
|
||||
'edit' => EditGenre::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Genres/Pages/CreateGenre.php
Normal file
11
app/Filament/Resources/Genres/Pages/CreateGenre.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Genres\Pages;
|
||||
|
||||
use App\Filament\Resources\Genres\GenreResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateGenre extends CreateRecord
|
||||
{
|
||||
protected static string $resource = GenreResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Genres/Pages/EditGenre.php
Normal file
19
app/Filament/Resources/Genres/Pages/EditGenre.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Genres\Pages;
|
||||
|
||||
use App\Filament\Resources\Genres\GenreResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditGenre extends EditRecord
|
||||
{
|
||||
protected static string $resource = GenreResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Genres/Pages/ListGenres.php
Normal file
19
app/Filament/Resources/Genres/Pages/ListGenres.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Genres\Pages;
|
||||
|
||||
use App\Filament\Resources\Genres\GenreResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListGenres extends ListRecords
|
||||
{
|
||||
protected static string $resource = GenreResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Filament/Resources/Genres/Schemas/GenreForm.php
Normal file
22
app/Filament/Resources/Genres/Schemas/GenreForm.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Genres\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class GenreForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('slug')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
}
|
||||
42
app/Filament/Resources/Genres/Tables/GenresTable.php
Normal file
42
app/Filament/Resources/Genres/Tables/GenresTable.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Genres\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class GenresTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/Languages/LanguageResource.php
Normal file
50
app/Filament/Resources/Languages/LanguageResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Languages;
|
||||
|
||||
use App\Filament\Resources\Languages\Pages\CreateLanguage;
|
||||
use App\Filament\Resources\Languages\Pages\EditLanguage;
|
||||
use App\Filament\Resources\Languages\Pages\ListLanguages;
|
||||
use App\Filament\Resources\Languages\Schemas\LanguageForm;
|
||||
use App\Filament\Resources\Languages\Tables\LanguagesTable;
|
||||
use App\Models\Language;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class LanguageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Language::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::Language;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return LanguageForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return LanguagesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListLanguages::route('/'),
|
||||
'create' => CreateLanguage::route('/create'),
|
||||
'edit' => EditLanguage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Languages/Pages/CreateLanguage.php
Normal file
11
app/Filament/Resources/Languages/Pages/CreateLanguage.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Resources\Languages\LanguageResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateLanguage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Languages/Pages/EditLanguage.php
Normal file
19
app/Filament/Resources/Languages/Pages/EditLanguage.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Resources\Languages\LanguageResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditLanguage extends EditRecord
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Languages/Pages/ListLanguages.php
Normal file
19
app/Filament/Resources/Languages/Pages/ListLanguages.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Resources\Languages\LanguageResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListLanguages extends ListRecords
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Filament/Resources/Languages/Schemas/LanguageForm.php
Normal file
23
app/Filament/Resources/Languages/Schemas/LanguageForm.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Languages\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class LanguageForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
TextInput::make('slug')
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
42
app/Filament/Resources/Languages/Tables/LanguagesTable.php
Normal file
42
app/Filament/Resources/Languages/Tables/LanguagesTable.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Languages\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class LanguagesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Modifications;
|
||||
|
||||
use App\Filament\Resources\Modifications\Pages\CreateModification;
|
||||
use App\Filament\Resources\Modifications\Pages\EditModification;
|
||||
use App\Filament\Resources\Modifications\Pages\ListModifications;
|
||||
use App\Filament\Resources\Modifications\Schemas\ModificationForm;
|
||||
use App\Filament\Resources\Modifications\Tables\ModificationsTable;
|
||||
use App\Models\Modification;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ModificationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Modification::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArchiveBox;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ModificationForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ModificationsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListModifications::route('/'),
|
||||
'create' => CreateModification::route('/create'),
|
||||
'edit' => EditModification::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Modifications\Pages;
|
||||
|
||||
use App\Filament\Resources\Modifications\ModificationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateModification extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ModificationResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Modifications\Pages;
|
||||
|
||||
use App\Filament\Resources\Modifications\ModificationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditModification extends EditRecord
|
||||
{
|
||||
protected static string $resource = ModificationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Modifications\Pages;
|
||||
|
||||
use App\Filament\Resources\Modifications\ModificationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListModifications extends ListRecords
|
||||
{
|
||||
protected static string $resource = ModificationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Modifications\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ModificationForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
TextInput::make('slug')
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Modifications\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ModificationsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Platforms/Pages/CreatePlatform.php
Normal file
11
app/Filament/Resources/Platforms/Pages/CreatePlatform.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Platforms\Pages;
|
||||
|
||||
use App\Filament\Resources\Platforms\PlatformResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePlatform extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PlatformResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Platforms/Pages/EditPlatform.php
Normal file
19
app/Filament/Resources/Platforms/Pages/EditPlatform.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Platforms\Pages;
|
||||
|
||||
use App\Filament\Resources\Platforms\PlatformResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPlatform extends EditRecord
|
||||
{
|
||||
protected static string $resource = PlatformResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Platforms/Pages/ListPlatforms.php
Normal file
19
app/Filament/Resources/Platforms/Pages/ListPlatforms.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Platforms\Pages;
|
||||
|
||||
use App\Filament\Resources\Platforms\PlatformResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPlatforms extends ListRecords
|
||||
{
|
||||
protected static string $resource = PlatformResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/Platforms/PlatformResource.php
Normal file
50
app/Filament/Resources/Platforms/PlatformResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Platforms;
|
||||
|
||||
use App\Filament\Resources\Platforms\Pages\CreatePlatform;
|
||||
use App\Filament\Resources\Platforms\Pages\EditPlatform;
|
||||
use App\Filament\Resources\Platforms\Pages\ListPlatforms;
|
||||
use App\Filament\Resources\Platforms\Schemas\PlatformForm;
|
||||
use App\Filament\Resources\Platforms\Tables\PlatformsTable;
|
||||
use App\Models\Platform;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PlatformResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Platform::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::Cube;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return PlatformForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return PlatformsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListPlatforms::route('/'),
|
||||
'create' => CreatePlatform::route('/create'),
|
||||
'edit' => EditPlatform::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
26
app/Filament/Resources/Platforms/Schemas/PlatformForm.php
Normal file
26
app/Filament/Resources/Platforms/Schemas/PlatformForm.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Platforms\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class PlatformForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(100),
|
||||
TextInput::make('slug')
|
||||
->required()
|
||||
->unique( ignoreRecord: true )
|
||||
->maxLength(100),
|
||||
TextInput::make('short_name')
|
||||
->default(null)
|
||||
->maxLength(30),
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
app/Filament/Resources/Platforms/Tables/PlatformsTable.php
Normal file
45
app/Filament/Resources/Platforms/Tables/PlatformsTable.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Platforms\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PlatformsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('short_name')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Statuses/Pages/CreateStatus.php
Normal file
11
app/Filament/Resources/Statuses/Pages/CreateStatus.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Statuses\Pages;
|
||||
|
||||
use App\Filament\Resources\Statuses\StatusResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateStatus extends CreateRecord
|
||||
{
|
||||
protected static string $resource = StatusResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Statuses/Pages/EditStatus.php
Normal file
19
app/Filament/Resources/Statuses/Pages/EditStatus.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Statuses\Pages;
|
||||
|
||||
use App\Filament\Resources\Statuses\StatusResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditStatus extends EditRecord
|
||||
{
|
||||
protected static string $resource = StatusResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Statuses/Pages/ListStatuses.php
Normal file
19
app/Filament/Resources/Statuses/Pages/ListStatuses.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Statuses\Pages;
|
||||
|
||||
use App\Filament\Resources\Statuses\StatusResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListStatuses extends ListRecords
|
||||
{
|
||||
protected static string $resource = StatusResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Filament/Resources/Statuses/Schemas/StatusForm.php
Normal file
23
app/Filament/Resources/Statuses/Schemas/StatusForm.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Statuses\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class StatusForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
TextInput::make('slug')
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Filament/Resources/Statuses/StatusResource.php
Normal file
50
app/Filament/Resources/Statuses/StatusResource.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Statuses;
|
||||
|
||||
use App\Filament\Resources\Statuses\Pages\CreateStatus;
|
||||
use App\Filament\Resources\Statuses\Pages\EditStatus;
|
||||
use App\Filament\Resources\Statuses\Pages\ListStatuses;
|
||||
use App\Filament\Resources\Statuses\Schemas\StatusForm;
|
||||
use App\Filament\Resources\Statuses\Tables\StatusesTable;
|
||||
use App\Models\Status;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class StatusResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Status::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ChartBar;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return StatusForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return StatusesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListStatuses::route('/'),
|
||||
'create' => CreateStatus::route('/create'),
|
||||
'edit' => EditStatus::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/Filament/Resources/Statuses/Tables/StatusesTable.php
Normal file
42
app/Filament/Resources/Statuses/Tables/StatusesTable.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Statuses\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class StatusesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
59
app/Helpers/EntryHelpers.php
Normal file
59
app/Helpers/EntryHelpers.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EntryHelpers {
|
||||
|
||||
/**
|
||||
* Create a unique slug.
|
||||
*
|
||||
* @param string $title
|
||||
* @param class-string $model The model with a 'slug' field.
|
||||
* @param int|null $ignoreId Ignore specific ID, like when edition
|
||||
*
|
||||
* @return string The original slug if no duplicates, otherwise with a number at the end.
|
||||
*/
|
||||
public static function uniqueSlug(string $title, string $model, ?int $ignoreId = null ): string {
|
||||
|
||||
$slug = Str::slug($title);
|
||||
$baseSlug = $slug;
|
||||
$i = 1;
|
||||
while(
|
||||
$model::where( 'slug', $slug )
|
||||
->when($ignoreId, fn($q) => $q->where('id', '!=', $ignoreId))
|
||||
->exists() && $i < 100
|
||||
){
|
||||
$slug = $baseSlug . '-' . $i++;
|
||||
}
|
||||
|
||||
if( $i >= 100 ){
|
||||
$slug = Str::uuid(); // Fallback...
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build complete title.
|
||||
*
|
||||
* @param string $section
|
||||
* @param array $fields
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function buildCompleteTitle( string $section, array $fields = [] ){
|
||||
|
||||
return match ($section) {
|
||||
'translations' => sprintf('%s (%s Translation) %s', $fields['entry_title'] ?? $fields['game_name'], $fields['languages_string'], $fields['platform_name']),
|
||||
'romhacks' => sprintf('%s (%s) Romhack', $fields['entry_title'], $fields['platform_name']),
|
||||
'homebrew' => sprintf('%s (%s) Homebrew', $fields['game_name'], $fields['platform_name']),
|
||||
'utilities' => sprintf('%s - Utility', $fields['entry_title']),
|
||||
'documents' => sprintf('%s - Document', $fields['entry_title']),
|
||||
'lua-scripts' => sprintf('%s (%s) LUA Script', $fields['entry_title'], $fields['platform_name']),
|
||||
'tutorials' => sprintf('%s - Tutorial', $fields['entry_title']),
|
||||
default => $fields['entry_title'],
|
||||
};
|
||||
}
|
||||
}
|
||||
49
app/Helpers/FormHelpers.php
Normal file
49
app/Helpers/FormHelpers.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class FormHelpers {
|
||||
|
||||
private static array $formWords = [
|
||||
'translations' => [
|
||||
'page_title' => "Submit a translation",
|
||||
'about_the' => "About the translation",
|
||||
'entry_title' => "Custom title",
|
||||
'entry_title_helper' => "If the translation have a custom title. If not, leave it blank and the game title will be taken.",
|
||||
'version' => "Patch version",
|
||||
'status' => "Status",
|
||||
'release_date' => "Release date",
|
||||
'release_date_helper' => "If only initial release exist, the release date.",
|
||||
'description' => "Description",
|
||||
'about_game' => "Game Information",
|
||||
'attachments' => "Attachments",
|
||||
'authors' => "Team members",
|
||||
'related_links' => "Related links",
|
||||
'release_site' => "Release site",
|
||||
'release_site_helper' => "Project entry on site/blog/forum/Github.",
|
||||
'youtube_video' => "YouTube video",
|
||||
],
|
||||
'romhacks' => [
|
||||
'page_title' => "Submit a romhack",
|
||||
'about_the' => "About the romhack",
|
||||
'entry_title' => "Hack title",
|
||||
'type_of_hack' => "Type of hack",
|
||||
'version' => "Patch version",
|
||||
'status' => "Status",
|
||||
'release_date' => "Release date",
|
||||
'release_date_helper' => "If only initial release exist, the release date.",
|
||||
'description' => "Description",
|
||||
'about_game' => "Game Information",
|
||||
'attachments' => "Attachments",
|
||||
'authors' => "Team members",
|
||||
'related_links' => "Related links",
|
||||
'release_site' => "Release site",
|
||||
'release_site_helper' => "Project entry on site/blog/forum/Github.",
|
||||
'youtube_video' => "YouTube video",
|
||||
],
|
||||
];
|
||||
|
||||
public static function getEntryFormWords( string $section ){
|
||||
return self::$formWords[$section] ?? [];
|
||||
}
|
||||
}
|
||||
37
app/Helpers/XenForoHelpers.php
Normal file
37
app/Helpers/XenForoHelpers.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Auth\XenForoUser;
|
||||
|
||||
class XenForoHelpers {
|
||||
|
||||
const array XF_AVATAR_COLORS = ['#FF6B6B','#FF9F43','#48DBFB','#1DD1A1','#5F27CD','#341f97','#EE5A24','#009432'];
|
||||
|
||||
/**
|
||||
* Have XenForo default profile picture letter.
|
||||
*
|
||||
* @param ?XenForoUser $user If null, default : actual user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAvatarLetter( ?XenForoUser $user = null ): string {
|
||||
if( $user === null ) {
|
||||
$user = \Auth::user();
|
||||
if( $user === null ) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return strtoupper(mb_substr($user->data->username, 0, 1));
|
||||
}
|
||||
|
||||
public static function getAvatarColor( ?XenForoUser $user = null ): string {
|
||||
if( $user === null ) {
|
||||
$user = \Auth::user();
|
||||
if( $user === null ) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return self::XF_AVATAR_COLORS[ crc32( $user->data->username ) % count( self::XF_AVATAR_COLORS ) ];
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
36
app/Http/Controllers/EntryController.php
Normal file
36
app/Http/Controllers/EntryController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EntryController extends Controller
|
||||
{
|
||||
|
||||
private const SECTION_TYPES = [ 'translations', 'romhacks', 'homebrew', 'utilities', 'documents', 'lua-scripts', 'tutorials' ];
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$entries = Entry::published()
|
||||
->with(['game.platform', 'platform'])
|
||||
->latest('published_at')
|
||||
->paginate(30);
|
||||
|
||||
return view('entries.index', compact('entries'));
|
||||
}
|
||||
|
||||
public function show(string $section, Entry $entry): View
|
||||
{
|
||||
if( ! in_array($section, self::SECTION_TYPES) )
|
||||
abort(404);
|
||||
|
||||
if( $entry->type !== $section )
|
||||
abort(404);
|
||||
|
||||
return view('entries.show', compact('entry', 'section'));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
71
app/Http/Controllers/FileServerController.php
Normal file
71
app/Http/Controllers/FileServerController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EntryFile;
|
||||
use App\Services\FileServersService;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FileServerController extends Controller {
|
||||
|
||||
public function __construct(private FileServersService $fs) {}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function uploadChunk(Request $request, string $section): JsonResponse {
|
||||
$request->validate([
|
||||
'file' => 'required|file',
|
||||
'file_uuid' => 'required|string|max:128',
|
||||
'current_chunk' => 'required|integer|min:0',
|
||||
'total_chunks' => 'required|integer|min:1',
|
||||
'filename' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$type = $section;
|
||||
$fileUuid = $request->input('file_uuid');
|
||||
$currentChunk = (int) $request->input('current_chunk');
|
||||
$totalChunks = (int) $request->input('total_chunks');
|
||||
$filename = $request->input('filename');
|
||||
|
||||
$data = $this->fs->uploadChunk(
|
||||
$request->file('file'),
|
||||
$fileUuid,
|
||||
$currentChunk,
|
||||
$totalChunks,
|
||||
$filename,
|
||||
$type
|
||||
);
|
||||
|
||||
if( !isset( $data['file'] ) || $data['file'] === false ){
|
||||
$data['finished'] = false;
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
\Cache::put("uploaded_file_{$fileUuid}", [
|
||||
'uuid' => $fileUuid,
|
||||
'type' => $type,
|
||||
'filename' => $filename,
|
||||
'filepath' => $data['file_path'],
|
||||
'filesize' => $data['file']['size'],
|
||||
'favorite_server' => $data['favorite_server'],
|
||||
'favorite_at' => time()
|
||||
], now()->addHours(2) );
|
||||
|
||||
$data['finished'] = true;
|
||||
return response()->json($data);
|
||||
|
||||
}
|
||||
|
||||
public function download(Request $request, int $entry_id, EntryFile $file ) {
|
||||
|
||||
if( $file->entry_id != $entry_id ) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// TODO: DL Count.
|
||||
return redirect( $this->fs->getDownloadFileUrl( $file) );
|
||||
}
|
||||
}
|
||||
15
app/Http/Controllers/HomeController.php
Normal file
15
app/Http/Controllers/HomeController.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
|
||||
public function index(): View {
|
||||
return view('home');
|
||||
}
|
||||
|
||||
}
|
||||
265
app/Http/Controllers/SubmissionController.php
Normal file
265
app/Http/Controllers/SubmissionController.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\SubmissionException;
|
||||
use App\Helpers\FormHelpers;
|
||||
use App\Http\Requests\StoreEntryRequest;
|
||||
use App\Models\Author;
|
||||
use App\Models\Entry;
|
||||
use App\Models\EntryFile;
|
||||
use App\Models\EntryGallery;
|
||||
use App\Models\EntryHash;
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Language;
|
||||
use App\Models\Modification;
|
||||
use App\Models\Platform;
|
||||
use App\Models\Status;
|
||||
use App\Services\SubmissionsService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SubmissionController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(private SubmissionsService $services){}
|
||||
|
||||
public function create(Request $request, string $section)
|
||||
{
|
||||
$data = [
|
||||
'entry' => new Entry(),
|
||||
'section' => $section,
|
||||
'words' => FormHelpers::getEntryFormWords($section),
|
||||
'isEdit' => false,
|
||||
'oldModifications' => old( 'modifications', [] ),
|
||||
'oldLanguages' => old( 'languages', [] ),
|
||||
'oldFilesArray' => $this->services->prepareOldFiles( null )
|
||||
];
|
||||
|
||||
if( $data['words'] === [] )
|
||||
abort(500);
|
||||
|
||||
if( section_must_be( 'romhacks', $section ) ){
|
||||
$data['modifications'] = Modification::orderBy('name')->get();
|
||||
}
|
||||
if( section_must_be( [ 'romhacks', 'translations' ], $section ) ){
|
||||
$data['statuses'] = Status::orderBy('id')->get();
|
||||
}
|
||||
|
||||
return view('submissions.create', $data);
|
||||
}
|
||||
|
||||
|
||||
public function edit(Request $request, string $section, Entry $entry){
|
||||
|
||||
if( $entry->type !== $section )
|
||||
abort(404);
|
||||
|
||||
$data = [
|
||||
'entry' => $entry,
|
||||
'section' => $section,
|
||||
'words' => FormHelpers::getEntryFormWords($section),
|
||||
'isEdit' => true,
|
||||
'oldModifications' => old('modifications', $entry->modifications->pluck('id')->toArray() ?? [] ),
|
||||
'oldLanguages' => old('languages', $entry->languages->pluck('id')->toArray() ?? [] ),
|
||||
'oldFilesArray' => $this->services->prepareOldFiles( $entry )
|
||||
];
|
||||
|
||||
if( $data['words'] === [] )
|
||||
abort(500);
|
||||
|
||||
if( section_must_be( 'romhacks', $section ) ){
|
||||
$data['modifications'] = Modification::orderBy('name')->get();
|
||||
}
|
||||
if( section_must_be( [ 'romhacks', 'translations' ], $section ) ){
|
||||
$data['statuses'] = Status::orderBy('id')->get();
|
||||
}
|
||||
|
||||
return view('submissions.edit', $data);
|
||||
}
|
||||
|
||||
public function store(StoreEntryRequest $request, string $section){
|
||||
|
||||
try {
|
||||
$entry = $this->services->storeEntry($request, $section);
|
||||
|
||||
return match ($entry->state) {
|
||||
'published' => redirect()->route('entries.show', ['section' => $section, 'entry' => $entry->slug])->with('success', "Your entry has been published."),
|
||||
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
|
||||
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
|
||||
};
|
||||
} catch ( SubmissionException $e ) {
|
||||
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
|
||||
} catch ( \Exception $e ) {
|
||||
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function update(StoreEntryRequest $request, string $section, Entry $entry)
|
||||
{
|
||||
if( $entry->type !== $section ) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$gameId = null;
|
||||
if( !$request->input('game_id') ){
|
||||
if( $request->input('new-game-title') && $request->input('new-game-platform') && $request->input('new-game-genre') ){
|
||||
$platform = Platform::find($request->input('new-game-platform'));
|
||||
$genre = Genre::find($request->input('new-game-genre'));
|
||||
$game = Game::create([
|
||||
'name' => $request->input('new-game-title'),
|
||||
'slug' => Str::slug($request->input('new-game-title')),
|
||||
'platform_id' => $platform->id,
|
||||
'genre_id' => $genre->id,
|
||||
]);
|
||||
$gameId = $game->id;
|
||||
}
|
||||
} else {
|
||||
$gameId = $request->input('game_id');
|
||||
}
|
||||
|
||||
$mainImage = $entry->main_image;
|
||||
if ( $request->hasFile('main-image') ) {
|
||||
if ( $mainImage ) {
|
||||
Storage::disk('public')->delete($mainImage);
|
||||
}
|
||||
$mainImage = $request->file('main-image')->store('entries/main_images', 'public');
|
||||
} elseif ( $request->input('remove_main_image') === '1' ) {
|
||||
if ( $mainImage ) {
|
||||
Storage::disk('public')->delete($mainImage);
|
||||
}
|
||||
$mainImage = null;
|
||||
}
|
||||
|
||||
$staffCredits = collect($request->input('credits', []))
|
||||
->filter(fn($item) => isset($item['name']) || isset($item['description']))
|
||||
->map(function ($item) {
|
||||
$name = trim($item['name'] ?? '');
|
||||
$description = trim($item['description'] ?? '');
|
||||
if ($name === '' && $description === '') {
|
||||
return null;
|
||||
}
|
||||
return trim($name . ($name !== '' && $description !== '' ? ' — ' : '') . $description);
|
||||
})
|
||||
->filter()
|
||||
->implode("\n");
|
||||
|
||||
$fields = [
|
||||
'type' => $section,
|
||||
'title' => $request->input('entry_title'),
|
||||
'slug' => $request->input('slug') ?? Str::slug($request->input('entry_title', '')),
|
||||
'description' => $request->input('description'),
|
||||
'main_image' => $mainImage,
|
||||
'state' => $request->input('submit-state', 'draft'),
|
||||
'game_id' => $gameId,
|
||||
'status_id' => $request->input('status'),
|
||||
'version' => $request->input('version'),
|
||||
'release_date' => $request->input('release-date'),
|
||||
'staff_credits' => $staffCredits ?: null,
|
||||
'relevant_link' => $request->input('release_site'),
|
||||
'youtube_link' => $request->input('youtube_video'),
|
||||
];
|
||||
|
||||
$entry->update($fields);
|
||||
|
||||
$entry->hashes()->delete();
|
||||
foreach ( $request->input('hashes', []) as $hash ) {
|
||||
if( !isset($hash['filename'], $hash['crc32'], $hash['sha1'], $hash['verified']) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryHash::create([
|
||||
'entry_id' => $entry->id,
|
||||
'filename' => $hash['filename'],
|
||||
'hash_crc32' => $hash['crc32'],
|
||||
'hash_sha1' => $hash['sha1'],
|
||||
'verified' => $hash['verified'],
|
||||
]);
|
||||
}
|
||||
|
||||
$authorIds = [];
|
||||
foreach ( $request->input('authors', []) as $authorId ) {
|
||||
$author = Author::find($authorId);
|
||||
if( $author ) {
|
||||
$authorIds[] = $author->id;
|
||||
}
|
||||
}
|
||||
foreach( $request->input('new-authors', []) as $authorName ) {
|
||||
$authorName = trim($authorName);
|
||||
if ($authorName === '') continue;
|
||||
|
||||
$author = Author::firstOrCreate(
|
||||
['slug' => Str::slug($authorName)],
|
||||
['name' => $authorName],
|
||||
);
|
||||
$authorIds[] = $author->id;
|
||||
}
|
||||
$entry->authors()->sync(array_values(array_unique($authorIds)));
|
||||
|
||||
if( section_must_be( 'romhacks', $section ) ){
|
||||
$entry->modifications()->sync($request->input('modifications', []));
|
||||
} else {
|
||||
$entry->modifications()->sync([]);
|
||||
}
|
||||
|
||||
$entry->languages()->sync($request->input('languages', []));
|
||||
|
||||
$existingFileUuids = $request->input('existing_file_ids', []);
|
||||
if (!is_array($existingFileUuids)) {
|
||||
$existingFileUuids = [];
|
||||
}
|
||||
$entry->files()->whereNotIn('file_uuid', $existingFileUuids)->delete();
|
||||
|
||||
foreach ( $request->input('file_ids', []) as $file_uuid ) {
|
||||
$fileData = Cache::pull("uploaded_file_{$file_uuid}");
|
||||
if( ! $fileData ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryFile::create([
|
||||
'entry_id' => $entry->id,
|
||||
'file_uuid' => $fileData['uuid'],
|
||||
'filename' => $fileData['filename'],
|
||||
'filepath' => $fileData['filepath'],
|
||||
'favorite_server' => $fileData['favorite_server'],
|
||||
'favorite_at' => \DateTimeImmutable::createFromTimestamp( $fileData['favorite_at'] ),
|
||||
'filesize' => $fileData['filesize'],
|
||||
'state' => 'public'
|
||||
]);
|
||||
}
|
||||
|
||||
$existingGalleryIds = $request->input('existing_gallery_ids', []);
|
||||
if (!is_array($existingGalleryIds)) {
|
||||
$existingGalleryIds = [];
|
||||
}
|
||||
$entry->gallery()->whereNotIn('id', $existingGalleryIds)->get()->each(function ($gallery) {
|
||||
if ($gallery->image) {
|
||||
Storage::disk('public')->delete($gallery->image);
|
||||
}
|
||||
$gallery->delete();
|
||||
});
|
||||
|
||||
foreach ( $request->file('gallery', [] ) as $galleryFile ){
|
||||
if( !$galleryFile->isValid() ){
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $galleryFile->store('entries/gallery/' . $entry->id, 'public');
|
||||
EntryGallery::create([
|
||||
'entry_id' => $entry->id,
|
||||
'image' => $path
|
||||
]);
|
||||
}
|
||||
|
||||
return match( $entry->state ){
|
||||
'published' => redirect()->route('entries.show', [ 'section' => $section, 'entry' => $entry->slug ])->with('success', "Your entry has been published."),
|
||||
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
|
||||
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
|
||||
};
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/TemporaryFileController.php
Normal file
27
app/Http/Controllers/TemporaryFileController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\TemporaryFileUploadRequest;
|
||||
use App\Services\TemporaryFileService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TemporaryFileController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(private TemporaryFileService $services) {}
|
||||
|
||||
public function upload(TemporaryFileUploadRequest $request){
|
||||
$file = $request->file('file');
|
||||
if( !$file || !$file->isValid()){
|
||||
response()->json( ['path' => null], 400);
|
||||
}
|
||||
|
||||
$path = $this->services->uploadFile( $file );
|
||||
if( !$path ){
|
||||
response()->json( ['path' => null], 500);
|
||||
}
|
||||
|
||||
return response()->json(['path' => $path]);
|
||||
}
|
||||
}
|
||||
43
app/Http/Middleware/CheckXenForoPermissions.php
Normal file
43
app/Http/Middleware/CheckXenForoPermissions.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckXenForoPermissions
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$permissions ): Response
|
||||
{
|
||||
if( !\Auth::check() )
|
||||
return redirect()->to(config('app.forum_url') . '/login' );
|
||||
|
||||
if( empty($permissions) ) // No permissions needed.
|
||||
return $next($request);
|
||||
|
||||
foreach ($permissions as $permissionStr) {
|
||||
[$group, $permission] = explode('.', $permissionStr);
|
||||
|
||||
if( !\Auth::user()->can($group, $permission) )
|
||||
return $this->deny($request, $permission);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function deny(Request $request, string $permission): Response
|
||||
{
|
||||
if($request->expectsJson())
|
||||
return \response()->json(['error' => 'forbidden'], 403);
|
||||
|
||||
return response()->view('pages.forbidden', [
|
||||
'permission' => $permission,
|
||||
], 403 );
|
||||
}
|
||||
}
|
||||
121
app/Http/Requests/StoreEntryRequest.php
Normal file
121
app/Http/Requests/StoreEntryRequest.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\PublicFileExists;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoreEntryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// TODO: Change it by role.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
|
||||
$newGameTitle = trim((string) $this->input('new-game-title', ''));
|
||||
$newGamePlatform = $this->input('new-game-platform');
|
||||
$newGameGenre = $this->input('new-game-genre');
|
||||
|
||||
$this->merge([
|
||||
'game_id' => $this->input('game_id') !== '' ? $this->input('game_id') : null,
|
||||
'new-game-title' => $newGameTitle !== '' ? $newGameTitle : null,
|
||||
'new-game-platform' => $newGamePlatform !== '' ? $newGamePlatform : null,
|
||||
'new-game-genre' => $newGameGenre !== '' ? $newGameGenre : null,
|
||||
'gallery' => $this->input('gallery') !== '' ? $this->input('gallery') : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [];
|
||||
$section = $this->route('section');
|
||||
|
||||
$rules['files_uuid'] = 'array|required|min:1';
|
||||
$rules['files_uuid.*'] = 'string';
|
||||
|
||||
if( section_must_not_be( 'translations', $section ) ){
|
||||
$rules['entry_title'] = "required|string|max:255";
|
||||
} else {
|
||||
$rules['entry_title'] = "nullable|string|max:255";
|
||||
}
|
||||
|
||||
if( section_must_be( 'romhacks', $section ) ){
|
||||
$rules['modifications'] = 'array|required|min:1';
|
||||
$rules['modifications.*'] = 'integer|exists:modifications,id';
|
||||
}
|
||||
|
||||
$rules['version'] = 'required|string|max:50';
|
||||
$rules['release-date'] = 'required|date';
|
||||
$rules['status'] = 'required|integer|exists:statuses,id';
|
||||
$rules['description'] = 'required|string';
|
||||
|
||||
if( section_must_be( ['romhacks', 'translations' ], $section ) ){
|
||||
$rules['game_id'] = 'required_without:new-game-title|nullable|integer|exists:games,id';
|
||||
$rules['new-game-title'] = 'required_without:game_id|nullable|string|max:255';
|
||||
$rules['new-game-platform'] = 'required_with:new-game-title|nullable|integer|exists:platforms,id';
|
||||
$rules['new-game-genre'] = 'required_with:new-game-title|integer|nullable|exists:genres,id';
|
||||
}
|
||||
|
||||
|
||||
$rules['hashes'] = 'array|required|min:1';
|
||||
$rules['hashes.*.filename'] = 'required|string|max:512';
|
||||
$rules['hashes.*.hash_crc32'] = 'required|string|max:512';
|
||||
$rules['hashes.*.hash_sha1'] = 'required|string|max:512';
|
||||
$rules['hashes.*.verified'] = 'required|string|max:512';
|
||||
|
||||
$rules['languages'] = 'array|required|min:1';
|
||||
$rules['languages.*'] = 'integer|exists:languages,id';
|
||||
|
||||
$rules['main-image'] = [ 'required', 'string', new PublicFileExists ];
|
||||
$rules['gallery'] = 'array|required|min:1';
|
||||
$rules['gallery.*'] = [ 'string', new PublicFileExists ];
|
||||
|
||||
$rules['authors'] = 'array|required_without:new-authors|min:1';
|
||||
$rules['authors.*'] = 'integer|exists:authors,id';
|
||||
$rules['new-authors'] = 'array|required_without:authors|min:1';
|
||||
$rules['new-authors.*'] = 'string|max:255';
|
||||
|
||||
$rules['staff_credits'] = 'nullable|json';
|
||||
$rules['release_site'] = 'nullable|url|max:500';
|
||||
$rules['youtube_video'] = 'nullable|url|max:500';
|
||||
|
||||
$rules['submit-state'] = 'required|string|in:draft,pending,published';
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'entry_title.required' => 'Please provide an entry title.',
|
||||
'slug.unique' => 'An entry with this title already exists. Please choose another title.',
|
||||
'file_ids.required' => 'Please upload at least one file.',
|
||||
'file_ids.min' => 'Please upload at least one file.',
|
||||
'hashes.required' => 'Please add at least one hash.',
|
||||
'hashes.min' => 'Please add at least one hash.',
|
||||
'gallery.required' => 'Please add at least one screenshot.',
|
||||
'gallery.min' => 'Please add at least one screenshot.',
|
||||
'new-game-platform.required_without' => 'Please choose a platform for the new game.',
|
||||
'new-game-genre.required_without' => 'Please choose a genre for the new game.',
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/TemporaryFileUploadRequest.php
Normal file
29
app/Http/Requests/TemporaryFileUploadRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TemporaryFileUploadRequest 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.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'file' => 'required|file|max:100000'
|
||||
];
|
||||
}
|
||||
}
|
||||
149
app/Livewire/AuthorsSelector.php
Normal file
149
app/Livewire/AuthorsSelector.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Author;
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class AuthorsSelector extends Component
|
||||
{
|
||||
public string $search = '';
|
||||
|
||||
public bool $newAuthor = false;
|
||||
public string $newAuthorName = '';
|
||||
public array $selectedAuthors = []; // 'id', 'name'
|
||||
public array $newAuthors = [];
|
||||
|
||||
public bool $dropdown = false;
|
||||
|
||||
public function mount( array $oldAuthors = [], array $oldNewAuthors = [] ): void
|
||||
{
|
||||
$selectedAuthors = [];
|
||||
|
||||
if ( is_string($oldAuthors) || is_int($oldAuthors) ) {
|
||||
$authors = [ $oldAuthors ];
|
||||
}
|
||||
|
||||
if ( is_array($oldAuthors) && $oldAuthors !== [] ) {
|
||||
$firstItem = reset($oldAuthors);
|
||||
|
||||
if ( is_array($firstItem) && array_key_exists('id', $firstItem) ) {
|
||||
$selectedAuthors = $oldAuthors;
|
||||
} else {
|
||||
$authorRecords = Author::whereIn('id', array_filter($oldAuthors, fn($id) => $id !== null))->get()->keyBy('id');
|
||||
|
||||
foreach ($oldAuthors as $authorId) {
|
||||
if ( isset($authorRecords[$authorId]) ) {
|
||||
$selectedAuthors[] = [
|
||||
'id' => $authorRecords[$authorId]->id,
|
||||
'name' => $authorRecords[$authorId]->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( is_string($oldNewAuthors) || is_int($oldNewAuthors) ) {
|
||||
$newAuthors = [ $oldNewAuthors ];
|
||||
}
|
||||
|
||||
foreach ( (array) $oldNewAuthors as $name ) {
|
||||
if ( trim((string) $name) === '' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$selectedAuthors[] = [
|
||||
'id' => null,
|
||||
'name' => trim((string) $name),
|
||||
];
|
||||
}
|
||||
|
||||
$this->selectedAuthors = $selectedAuthors;
|
||||
$this->newAuthors = $oldNewAuthors;
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->dropdown = strlen($this->search) > 2;
|
||||
}
|
||||
|
||||
public function selectAuthor( int $id, string $name ): void
|
||||
{
|
||||
foreach ( $this->selectedAuthors as $author ) {
|
||||
if ( $author['id'] === $id ) {
|
||||
$this->search = '';
|
||||
$this->dropdown = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->selectedAuthors[] = [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
];
|
||||
|
||||
$this->search = '';
|
||||
$this->dropdown = false;
|
||||
|
||||
}
|
||||
|
||||
public function removeAuthor( int $i ): void
|
||||
{
|
||||
array_splice( $this->selectedAuthors, $i, 1 );
|
||||
}
|
||||
|
||||
public function addNewAuthor(): void
|
||||
{
|
||||
if( empty( trim( $this->newAuthorName ) ) )
|
||||
return;
|
||||
|
||||
foreach ( $this->selectedAuthors as $author ) {
|
||||
if( strtolower($author['name']) === strtolower( $this->newAuthorName ) ){
|
||||
$this->newAuthor = false;
|
||||
$this->newAuthorName = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->selectedAuthors[] = [
|
||||
'id' => null,
|
||||
'name' => trim( $this->newAuthorName ),
|
||||
];
|
||||
|
||||
$this->newAuthor = false;
|
||||
$this->newAuthorName = '';
|
||||
}
|
||||
|
||||
public function switchNewAuthor(): void
|
||||
{
|
||||
$this->newAuthor = !$this->newAuthor;
|
||||
$this->newAuthorName = '';
|
||||
$this->search = '';
|
||||
$this->dropdown = false;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$authors = collect();
|
||||
$data = [];
|
||||
|
||||
if( $this->dropdown && strlen( $this->search ) > 2 && !$this->newAuthor ){
|
||||
$ids = array_filter(array_column( $this->selectedAuthors, 'id' ), function ( $id ) {
|
||||
return !is_null( $id );
|
||||
});
|
||||
|
||||
$authors = Author::where( 'name', 'like', '%' . $this->search . '%' )
|
||||
->when( !empty( $ids ), function ( $query ) use ( $ids ) { $query->whereNotIn( 'id', $ids ); } )
|
||||
->orderBy( 'name' )
|
||||
->limit( 20 )
|
||||
->get();
|
||||
$data['authors'] = $authors;
|
||||
}
|
||||
|
||||
return view('livewire.authors-selector', $data );
|
||||
}
|
||||
}
|
||||
184
app/Livewire/GameSelector.php
Normal file
184
app/Livewire/GameSelector.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Game Selector for existing games and new games.
|
||||
*/
|
||||
class GameSelector extends Component
|
||||
{
|
||||
|
||||
public const int REQUIRED_CHARS = 3;
|
||||
|
||||
/**
|
||||
* If we are in new game mode.
|
||||
* @var bool
|
||||
*/
|
||||
public bool $newGame = false;
|
||||
|
||||
/**
|
||||
* Current user game search.
|
||||
* @var string
|
||||
*/
|
||||
public string $search = '';
|
||||
|
||||
/**
|
||||
* Existing game ID selected.
|
||||
* @var int|null
|
||||
*/
|
||||
public ?int $gameId = null;
|
||||
|
||||
/**
|
||||
* Existing game name selected or new game name.
|
||||
* @var string|null
|
||||
*/
|
||||
public ?string $gameName = null;
|
||||
|
||||
/**
|
||||
* New game platform ID.
|
||||
* @var int|null
|
||||
*/
|
||||
public ?int $gamePlatformId = null;
|
||||
|
||||
/**
|
||||
* New game genre ID.
|
||||
* @var int|null
|
||||
*/
|
||||
public ?int $gameGenreId = null;
|
||||
|
||||
/**
|
||||
* Existing game platform name or new game platform name.
|
||||
* @var string|null
|
||||
*/
|
||||
public ?string $platformName = null;
|
||||
|
||||
/**
|
||||
* Existing game genre name or new game genre name.
|
||||
* @var string|null
|
||||
*/
|
||||
public ?string $genreName = null;
|
||||
|
||||
/**
|
||||
* If dropdown must be rendered or not.
|
||||
* @var bool
|
||||
*/
|
||||
public bool $dropdown = false;
|
||||
|
||||
public function mount( ?int $gameId = null, ?string $newGameTitle = null, ?int $newGamePlatform = null, ?int $newGameGenre = null ): void
|
||||
{
|
||||
|
||||
// If we selected an existent game.
|
||||
if( $gameId ){
|
||||
$game = Game::with(['platform','genre'])->find($gameId);
|
||||
if( $game ){
|
||||
$this->gameId = $game->id;
|
||||
$this->gameName = $game->name;
|
||||
$this->platformName = $game->platform->name;
|
||||
$this->genreName = $game->genre->name;
|
||||
$this->search = $game->name;
|
||||
$this->newGame = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if( $newGameTitle || $newGamePlatform || $newGameGenre ){
|
||||
$this->newGame = true;
|
||||
$this->gameName = $newGameTitle;
|
||||
$this->gamePlatformId = is_numeric($newGamePlatform) ? (int) $newGamePlatform : null;
|
||||
$this->gameGenreId = is_numeric($newGameGenre) ? (int) $newGameGenre : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we update search bar.
|
||||
* @return void
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
if( $this->gameId ){
|
||||
$this->gameId = null;
|
||||
$this->gameName = null;
|
||||
$this->platformName = null;
|
||||
$this->genreName = null;
|
||||
}
|
||||
$this->dropdown = strlen($this->search) >= self::REQUIRED_CHARS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an existent game.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function selectGame( int $id, string $name ): void
|
||||
{
|
||||
$game = Game::with(['platform','genre'])->find($id);
|
||||
if( $game ){
|
||||
$this->gameId = $game->id;
|
||||
$this->gameName = $game->name;
|
||||
$this->platformName = $game->platform->name;
|
||||
$this->genreName = $game->genre->name;
|
||||
$this->search = $game->name;
|
||||
$this->dropdown = false;
|
||||
|
||||
$this->dispatch( 'game-selected', id: $id ); // Send an event to the JS part.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear existent game selection.
|
||||
* @return void
|
||||
*/
|
||||
public function clearGame(): void
|
||||
{
|
||||
$this->gameId = null;
|
||||
$this->gameName = null;
|
||||
$this->platformName = null;
|
||||
$this->genreName = null;
|
||||
$this->search = '';
|
||||
|
||||
$this->dispatch( 'game-selected', id: null ); // Send an event to the JS part.
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch mode.
|
||||
* @return void
|
||||
*/
|
||||
public function switchNewGame(): void
|
||||
{
|
||||
$this->clearGame();
|
||||
$this->newGame = !$this->newGame;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$games = collect();
|
||||
|
||||
// Need to search games for dropdown.
|
||||
if( $this->dropdown && strlen($this->search) >= self::REQUIRED_CHARS && $this->newGame === false ){
|
||||
$games = Game::with(['platform','genre'])
|
||||
->where('name', 'like', '%'.$this->search.'%')
|
||||
->orderBy('name')
|
||||
->limit(20)
|
||||
->get();
|
||||
}
|
||||
|
||||
$data = [ 'games' => $games, 'required_chars' => self::REQUIRED_CHARS ];
|
||||
if( $this->newGame === true ){ // If we want a new game, get platforms and genres.
|
||||
$data['platforms'] = Platform::orderBy('name')->get();
|
||||
$data['genres'] = Genre::orderBy('name')->get();
|
||||
}
|
||||
$data['hasOldNewGame'] = old('new-game-title') || old('new-game-platform') || old('new-game-genre');
|
||||
|
||||
return view('livewire.game-selector', $data );
|
||||
}
|
||||
}
|
||||
77
app/Livewire/HashesUpload.php
Normal file
77
app/Livewire/HashesUpload.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\EntryHash;
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Hashes uploader in submission form.
|
||||
* Upload hash and create fields.
|
||||
*
|
||||
* @phpstan-import-type HashObject from \App\Types\SubmissionTypes
|
||||
*/
|
||||
class HashesUpload extends Component
|
||||
{
|
||||
|
||||
/**
|
||||
* List of hashes.
|
||||
* @var list<HashObject> $hashes
|
||||
*/
|
||||
public array $hashes = [];
|
||||
|
||||
/**
|
||||
* Prepare old hashes.
|
||||
*
|
||||
* @param list<HashObject> $oldHashes
|
||||
* @return void
|
||||
*/
|
||||
public function mount( array $oldHashes = [] ): void
|
||||
{
|
||||
foreach ($oldHashes as $hash) {
|
||||
$this->addHash( $hash['filename'], $hash['hash_crc32'], $hash['hash_sha1'], $hash['verified'] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an hash to the list.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $crc32
|
||||
* @param string $sha1
|
||||
* @param string|null $verified
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addHash(string $filename, string $crc32, string $sha1, ?string $verified = null): void
|
||||
{
|
||||
$this->hashes[] = [
|
||||
'filename' => $filename,
|
||||
'hash_crc32' => $crc32,
|
||||
'hash_sha1' => $sha1,
|
||||
'verified' => $verified ?? "No-Intro" // TODO: Change it.
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific hash.
|
||||
*
|
||||
* @param int $index
|
||||
* @return void
|
||||
*/
|
||||
public function removeHash(int $index): void
|
||||
{
|
||||
array_splice($this->hashes, $index, 1);
|
||||
}
|
||||
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.hashes-upload');
|
||||
}
|
||||
}
|
||||
12
app/Models/Author.php
Normal file
12
app/Models/Author.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Author extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name', 'slug', 'user_id', 'website'
|
||||
];
|
||||
}
|
||||
93
app/Models/Entry.php
Normal file
93
app/Models/Entry.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Entry extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'title',
|
||||
'slug',
|
||||
'description',
|
||||
'main_image',
|
||||
'state',
|
||||
'featured',
|
||||
'game_id',
|
||||
'platform_id',
|
||||
'status_id',
|
||||
'version',
|
||||
'release_date',
|
||||
'staff_credits',
|
||||
'relevant_link',
|
||||
'youtube_link',
|
||||
'user_id',
|
||||
'complete_title',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $casts = [
|
||||
'featured' => 'boolean',
|
||||
'release_date' => 'date',
|
||||
];
|
||||
|
||||
public function scopePublished( Builder $query ): Builder {
|
||||
return $query->where( 'state', 'published' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return game link.
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function game(): BelongsTo {
|
||||
return $this->belongsTo(Game::class);
|
||||
}
|
||||
|
||||
public function platform(): BelongsTo {
|
||||
return $this->belongsTo(Platform::class);
|
||||
}
|
||||
|
||||
public function getRealPlatform(): ?Platform {
|
||||
return $this->game?->platform ?? $this->platform;
|
||||
}
|
||||
|
||||
public function status(): BelongsTo {
|
||||
return $this->belongsTo(Status::class );
|
||||
}
|
||||
|
||||
public function authors(): BelongsToMany {
|
||||
return $this->belongsToMany(Author::class, 'entry_authors');
|
||||
}
|
||||
|
||||
public function languages(): BelongsToMany {
|
||||
return $this->belongsToMany(Language::class, 'entry_languages');
|
||||
}
|
||||
|
||||
public function modifications(): BelongsToMany {
|
||||
return $this->belongsToMany( Modification::class, 'entry_modifications');
|
||||
}
|
||||
|
||||
public function files(): HasMany {
|
||||
return $this->hasMany(EntryFile::class)->orderBy('filename');
|
||||
}
|
||||
|
||||
public function gallery(): HasMany {
|
||||
return $this->hasMany(EntryGallery::class)->orderBy('id');
|
||||
}
|
||||
|
||||
public function hashes(): HasMany {
|
||||
return $this->hasMany(EntryHash::class);
|
||||
}
|
||||
|
||||
}
|
||||
23
app/Models/EntryFile.php
Normal file
23
app/Models/EntryFile.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EntryFile extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'entry_id', 'filename', 'filepath', 'favorite_server', 'favorite_at', 'filesize', 'state', 'file_uuid'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'favorite_at' => 'timestamp'
|
||||
];
|
||||
|
||||
public function entry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Entry::class);
|
||||
}
|
||||
|
||||
}
|
||||
11
app/Models/EntryGallery.php
Normal file
11
app/Models/EntryGallery.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EntryGallery extends Model
|
||||
{
|
||||
protected $fillable = ['entry_id','image'];
|
||||
|
||||
}
|
||||
18
app/Models/EntryHash.php
Normal file
18
app/Models/EntryHash.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EntryHash extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'entry_id', 'filename', 'hash_crc32', 'hash_sha1', 'verified'
|
||||
];
|
||||
|
||||
public function entry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Entry::class);
|
||||
}
|
||||
}
|
||||
24
app/Models/Game.php
Normal file
24
app/Models/Game.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Game extends Model
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = ['name', 'slug', 'platform_id', 'genre_id' ];
|
||||
|
||||
public function platform(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Platform::class);
|
||||
}
|
||||
|
||||
public function genre(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Genre::class);
|
||||
}
|
||||
}
|
||||
10
app/Models/Genre.php
Normal file
10
app/Models/Genre.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Genre extends Model
|
||||
{
|
||||
protected $fillable = [ 'name', 'slug' ];
|
||||
}
|
||||
10
app/Models/Language.php
Normal file
10
app/Models/Language.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Language extends Model
|
||||
{
|
||||
protected $fillable = [ 'name', 'slug' ];
|
||||
}
|
||||
10
app/Models/Modification.php
Normal file
10
app/Models/Modification.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Modification extends Model
|
||||
{
|
||||
protected $fillable = [ 'name', 'slug' ];
|
||||
}
|
||||
17
app/Models/Platform.php
Normal file
17
app/Models/Platform.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Platform extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name', 'slug', 'short_name'
|
||||
];
|
||||
|
||||
}
|
||||
10
app/Models/Status.php
Normal file
10
app/Models/Status.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Status extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'slug'];
|
||||
}
|
||||
32
app/Models/User.php
Normal file
32
app/Models/User.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Providers/AppServiceProvider.php
Normal file
27
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Auth\XenForoGuard;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
\Auth::extend('xenforo', function ($app, $name, array $config) {
|
||||
return new XenForoGuard($app['request']);
|
||||
});
|
||||
}
|
||||
}
|
||||
59
app/Providers/Filament/ManagePanelProvider.php
Normal file
59
app/Providers/Filament/ManagePanelProvider.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Pages\Dashboard;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Widgets\AccountWidget;
|
||||
use Filament\Widgets\FilamentInfoWidget;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class ManagePanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->default()
|
||||
->id('manage')
|
||||
->path('manage')
|
||||
->login()
|
||||
->colors([
|
||||
'primary' => Color::Amber,
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||
->pages([
|
||||
Dashboard::class,
|
||||
])
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
|
||||
->widgets([
|
||||
AccountWidget::class,
|
||||
FilamentInfoWidget::class,
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
PreventRequestForgery::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
app/Rules/PublicFileExists.php
Normal file
23
app/Rules/PublicFileExists.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||
|
||||
class PublicFileExists implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if(!Storage::disk('public')->exists($value) ) {
|
||||
$fail("The file {$value} does not exist.");
|
||||
}
|
||||
}
|
||||
}
|
||||
132
app/Services/FileServersService.php
Normal file
132
app/Services/FileServersService.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\EntryFile;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class FileServersService {
|
||||
|
||||
public const FAVORITE_SERVER_TIME = 3600; // In seconds.
|
||||
private const ZEUS_TOKEN_EXPIRATION = 600; // In seconds.
|
||||
|
||||
private string $apiKey;
|
||||
private array $servers;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('fileservers.secret_key');
|
||||
$this->servers = config('fileservers.servers');
|
||||
}
|
||||
|
||||
public function generateZeusToken( int $user_id, string $to, string $action ){
|
||||
|
||||
$info = [
|
||||
'user_id' => $user_id,
|
||||
'to' => $to,
|
||||
'action' => $action,
|
||||
'generated_at' => time(),
|
||||
'expires_at' => time() + self::ZEUS_TOKEN_EXPIRATION,
|
||||
'romhackplaza' => bin2hex(random_bytes(16)),
|
||||
];
|
||||
|
||||
$json = json_encode( $info );
|
||||
$sig = hash_hmac( 'sha256', $json, $this->apiKey );
|
||||
|
||||
$end = base64_encode( $json ) . "|" . $sig;
|
||||
return $end;
|
||||
}
|
||||
|
||||
public function getRandomServerKey(): string
|
||||
{
|
||||
$keys = array_keys($this->servers);
|
||||
return $keys[array_rand($keys)];
|
||||
}
|
||||
|
||||
public function getEntryFileServerKey( EntryFile $file ): string
|
||||
{
|
||||
$serverKey = null;
|
||||
|
||||
if( $file->favorite_server !== null && $file->favorite_at !== null ) {
|
||||
if( time() < $file->favorite_at + static::FAVORITE_SERVER_TIME ) {
|
||||
$serverKey = $file->favorite_server;
|
||||
}
|
||||
}
|
||||
if( $serverKey === null || !isset( $this->servers[$serverKey] ) ) {
|
||||
$serverKey = $this->getRandomServerKey();
|
||||
}
|
||||
|
||||
return $serverKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download URL requires 'filepath' and 'filename'.
|
||||
*
|
||||
* @param EntryFile $file
|
||||
* @return string
|
||||
*/
|
||||
public function getDownloadFileUrl( EntryFile $file ): string
|
||||
{
|
||||
$serverKey = $this->getEntryFileServerKey( $file );
|
||||
$url = $this->servers[$serverKey]['download'] ?? "#";
|
||||
if( $url === "#" )
|
||||
return $url;
|
||||
|
||||
return $url . "&" . http_build_query( [ 'filename' => $file->filename, 'filepath' => $file->filepath ] );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function uploadChunk(
|
||||
UploadedFile $chunk,
|
||||
string $fileUUID,
|
||||
int $currentChunk,
|
||||
int $totalChunks,
|
||||
string $filename,
|
||||
string $type
|
||||
){
|
||||
|
||||
// Define or get favorite server.
|
||||
if( $currentChunk === 0 ){
|
||||
$serverKey = $this->getRandomServerKey();
|
||||
Cache::put("favorite_server_{$fileUUID}", $serverKey, now()->addHours(2) );
|
||||
} else {
|
||||
$serverKey = Cache::get( "favorite_server_{$fileUUID}" );
|
||||
abort_if( !$serverKey, 422, "File upload expired, please retry." );
|
||||
}
|
||||
|
||||
$server = $this->servers[$serverKey];
|
||||
$filepath = $type . '/' . $fileUUID;
|
||||
|
||||
$response = Http::withHeaders([])
|
||||
->attach( 'file', file_get_contents( $chunk->getRealPath() ), $fileUUID )
|
||||
->post( $server['upload_chunk'], [
|
||||
'filepath' => $filepath,
|
||||
'filename' => $filename,
|
||||
'current_chunk' => $currentChunk,
|
||||
'total_chunks' => $totalChunks,
|
||||
// TODO : Must replace User ID
|
||||
'zeus' => $this->generateZeusToken( 0, $server['base_url'], "Uploadchunk" ),
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \RuntimeException( $response->body() );
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if( isset( $data['file'] ) && $data['file'] !== false ){
|
||||
Cache::forget( "favorite_server_{$fileUUID}" );
|
||||
}
|
||||
$data['favorite_server'] = $serverKey;
|
||||
$data['file_uuid'] = $fileUUID;
|
||||
$data['file_path'] = $filepath;
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
388
app/Services/SubmissionsService.php
Normal file
388
app/Services/SubmissionsService.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\SubmissionException;
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Http\Requests\StoreEntryRequest;
|
||||
use App\Models\Author;
|
||||
use App\Models\Entry;
|
||||
use App\Models\EntryFile;
|
||||
use App\Models\EntryGallery;
|
||||
use App\Models\EntryHash;
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Language;
|
||||
use App\Models\Modification;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type FSFileData from \App\Types\FSTypes
|
||||
*/
|
||||
class SubmissionsService {
|
||||
|
||||
/**
|
||||
* Request for store/edit.
|
||||
* @var StoreEntryRequest|null
|
||||
*/
|
||||
private ?StoreEntryRequest $request = null;
|
||||
|
||||
/**
|
||||
* Section for store/edit.
|
||||
* @var string|null
|
||||
*/
|
||||
private ?string $section = null;
|
||||
|
||||
/**
|
||||
* @return list<FSFileData>
|
||||
*/
|
||||
public function prepareOldFiles( ?Entry $entry = null ): array
|
||||
{
|
||||
if( $entry === null ){
|
||||
$files = old( 'files_uuid', [] );
|
||||
} else {
|
||||
$files = old( 'files_uuid', $entry->files->pluck('file_uuid')->toArray() );
|
||||
}
|
||||
|
||||
if( $files === [] )
|
||||
return [];
|
||||
|
||||
return array_map(
|
||||
function( string $uuid ) {
|
||||
$file = EntryFile::where('file_uuid', $uuid)->first();
|
||||
|
||||
if( $file )
|
||||
return [
|
||||
'name' => $file->filename,
|
||||
'totalChunks' => 0, // Already uploaded.
|
||||
'rawFile' => null,
|
||||
'progressValue' => 0,
|
||||
'currentChunk' => 0,
|
||||
'done' => true,
|
||||
'error' => null,
|
||||
'uuid' => $uuid
|
||||
];
|
||||
|
||||
$file = Cache::get("uploaded_file_{$uuid}");
|
||||
if( $file )
|
||||
return [
|
||||
'name' => $file['filename'],
|
||||
'totalChunks' => 0, // Already uploaded.
|
||||
'rawFile' => null,
|
||||
'progressValue' => 0,
|
||||
'currentChunk' => 0,
|
||||
'done' => true,
|
||||
'error' => null,
|
||||
'uuid' => $uuid
|
||||
];
|
||||
|
||||
return null;
|
||||
},
|
||||
$files );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StoreEntryRequest $request
|
||||
* @param string $section
|
||||
*
|
||||
* @return Entry
|
||||
* @throws SubmissionException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function storeEntry( StoreEntryRequest $request, string $section ){
|
||||
|
||||
// STEP 1 : Prepare basic fields.
|
||||
|
||||
$this->request = $request;
|
||||
$this->section = $section;
|
||||
$user_id = 0; // TODO: Replace that.
|
||||
|
||||
$entry = DB::transaction(function () use ( $user_id ) {
|
||||
|
||||
// STEP 2 : Create game.
|
||||
|
||||
$gameId = null;
|
||||
if( section_must_be( ['romhacks', 'translations'], $this->section ) ){
|
||||
$gameId = $this->Step2_CreateAndReturnGameId();
|
||||
}
|
||||
|
||||
// STEP 3 : Create Complete title.
|
||||
$completeTitle = $this->Step3_BuildCompleteTitle( $gameId );
|
||||
|
||||
// STEP 4 : Generate slug and entry title.
|
||||
$entrySlug = EntryHelpers::uniqueSlug( $completeTitle, Entry::class );
|
||||
|
||||
if( section_must_be( 'translations', $this->section ) &&
|
||||
!$this->request->input('entry_title') ){
|
||||
$entryTitle = Game::find($gameId)->name;
|
||||
} else {
|
||||
$entryTitle = $this->request->input('entry_title');
|
||||
}
|
||||
|
||||
// STEP 5 : Removed / Delayed.
|
||||
// $mainImage = $this->Step5_MoveMainImage();
|
||||
|
||||
// STEP 6 : Prepare entry fields and save entry.
|
||||
$fields = [
|
||||
'type' => $this->section,
|
||||
'title' => $entryTitle,
|
||||
'slug' => $entrySlug,
|
||||
'description' => $this->request->input('description'),
|
||||
'main_image' => $this->request->input('main-image'),
|
||||
'state' => $this->request->input('submit-state'),
|
||||
'game_id' => $gameId,
|
||||
'status_id' => $this->request->input('status'),
|
||||
'version' => $this->request->input('version'),
|
||||
'release_date' => $this->request->input('release-date'),
|
||||
'staff_credits' => $this->request->input('staff_credits'),
|
||||
'relevant_link' => $this->request->input('release_site'),
|
||||
'youtube_link' => $this->request->input('youtube_video'),
|
||||
'user_id' => $user_id,
|
||||
'complete_title' => $completeTitle,
|
||||
];
|
||||
|
||||
$entry = Entry::create( $fields );
|
||||
|
||||
// STEP 7 : Save entry fields.
|
||||
$this->Step7_SaveEntryFiles( $entry->id );
|
||||
|
||||
// STEP 8 : Save hashes.
|
||||
$this->Step8_SaveHashes( $entry->id );
|
||||
|
||||
// STEP 9 : Save Authors.
|
||||
$this->Step9_SaveAuthors( $entry );
|
||||
|
||||
// STEP 10 : Save Modifications.
|
||||
if( section_must_be( 'romhacks', $this->section ) ){
|
||||
$this->Step10_SaveRomhacksModifications( $entry );
|
||||
}
|
||||
|
||||
// STEP 11 : Save Languages
|
||||
$this->Step11_SaveLanguages( $entry );
|
||||
|
||||
// STEP 12 : Prepare Gallery images.
|
||||
$this->Step12a_PrepareGalleryImages( $entry );
|
||||
|
||||
return $entry;
|
||||
|
||||
});
|
||||
|
||||
// Step 12, Move main image and gallery.
|
||||
$this->Step12b_MoveMainImage( $entry );
|
||||
$this->Step12c_SaveGalleryImages( $entry );
|
||||
|
||||
return $entry;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*
|
||||
* @throws SubmissionException
|
||||
*/
|
||||
private function Step2_CreateAndReturnGameId(): int {
|
||||
|
||||
// Already existing game.
|
||||
if( $this->request->input('game_id') )
|
||||
return $this->request->input('game_id');
|
||||
|
||||
// Need to create a game.
|
||||
if( !$this->request->input('new-game-title') || !$this->request->input('new-game-platform') || !$this->request->input('new-game-genre') )
|
||||
throw new SubmissionException( "New game informations is missing" );
|
||||
|
||||
$platform = Platform::find( $this->request->input('new-game-platform') );
|
||||
$genre = Genre::find( $this->request->input('new-game-genre') );
|
||||
|
||||
if( !$platform || !$genre )
|
||||
throw new SubmissionException( "Incorrect game platform id" );
|
||||
|
||||
$gameSlug = EntryHelpers::uniqueSlug( $this->request->input('new-game-title'), Game::class );
|
||||
|
||||
$game = Game::create([
|
||||
'name' => trim( $this->request->input('new-game-title') ),
|
||||
'slug' => $gameSlug,
|
||||
'platform_id' => $platform->id,
|
||||
'genre_id' => $genre->id,
|
||||
]);
|
||||
|
||||
return $game->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and build complete title.
|
||||
*
|
||||
* @param int|null $gameId
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function Step3_BuildCompleteTitle( ?int $gameId = null ): string {
|
||||
$fields = [];
|
||||
|
||||
$fields['entry_title'] = $this->request->input('entry_title') ?? null;
|
||||
if( section_must_be( [ 'homebrew', 'translations' ], $this->section ) && $gameId ){
|
||||
$fields['game_name'] = Game::find( $gameId )->name;
|
||||
}
|
||||
if( section_must_be( 'translations', $this->section ) ) {
|
||||
$fields['languages_string'] = Language::whereIn('id', $this->request->input('languages', []))->pluck('name')->implode(', ');
|
||||
}
|
||||
if( section_must_be(['romhacks', 'homebrew', 'lua-scripts', 'tutorials'], $this->section ) ) {
|
||||
// TODO: Add single platform ID compatibility.
|
||||
$fields['platform_name'] = Game::find( $gameId )->platform->name;
|
||||
}
|
||||
|
||||
return EntryHelpers::buildCompleteTitle( $this->section, $fields );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $entryId
|
||||
*
|
||||
* @return void
|
||||
* @throws SubmissionException
|
||||
*/
|
||||
private function Step7_SaveEntryFiles( int $entryId ): void
|
||||
{
|
||||
foreach ( $this->request->input('files_uuid', [] ) as $uuid ) {
|
||||
$fileData = Cache::pull("uploaded_file_{$uuid}");
|
||||
if( !$fileData )
|
||||
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry." );
|
||||
|
||||
EntryFile::create([
|
||||
'entry_id' => $entryId,
|
||||
'file_uuid' => $uuid,
|
||||
'filename' => $fileData['filename'],
|
||||
'filepath' => $fileData['filepath'],
|
||||
'favorite_server' => $fileData['favorite_server'],
|
||||
'favorite_at' => \DateTimeImmutable::createFromTimestamp( $fileData['favorite_at'] ),
|
||||
'filesize' => $fileData['filesize'],
|
||||
'state' => 'public'
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $entryId
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function Step8_SaveHashes( int $entryId ): void
|
||||
{
|
||||
foreach ( $this->request->input('hashes', [] ) as $hash ) {
|
||||
if( !isset($hash['filename'], $hash['hash_crc32'], $hash['hash_sha1'], $hash['verified']) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryHash::create([
|
||||
'entry_id' => $entryId,
|
||||
'filename' => $hash['filename'],
|
||||
'hash_crc32' => $hash['hash_crc32'],
|
||||
'hash_sha1' => $hash['hash_sha1'],
|
||||
'verified' => $hash['verified'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entry $entry
|
||||
*
|
||||
* @return void
|
||||
* @throws SubmissionException
|
||||
*/
|
||||
private function Step9_SaveAuthors( Entry $entry ): void
|
||||
{
|
||||
// Existing authors.
|
||||
foreach ( $this->request->input('authors', [] ) as $authorId ) {
|
||||
$author = Author::find( $authorId );
|
||||
if( !$author )
|
||||
throw new SubmissionException( "Author {$authorId} does not exist." );
|
||||
$entry->authors()->attach( $author->id );
|
||||
}
|
||||
|
||||
// New Authors
|
||||
foreach ( $this->request->input('new-authors', [] ) as $authorName ) {
|
||||
$authorName = trim( $authorName );
|
||||
if( $authorName === '' )
|
||||
continue;
|
||||
|
||||
$author = Author::firstOrCreate(
|
||||
['slug' => EntryHelpers::uniqueSlug( $authorName, Author::class )],
|
||||
['name' => $authorName]
|
||||
);
|
||||
$entry->authors()->attach( $author->id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entry $entry
|
||||
*
|
||||
* @return void
|
||||
* @throws SubmissionException
|
||||
*/
|
||||
private function Step10_SaveRomhacksModifications( Entry $entry ): void
|
||||
{
|
||||
foreach ( $this->request->input('modifications', [] ) as $modificationId ) {
|
||||
$modification = Modification::find( $modificationId );
|
||||
if( !$modification )
|
||||
throw new SubmissionException( "Modification {$modificationId} does not exist." );
|
||||
$entry->modifications()->attach( $modification->id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entry $entry
|
||||
*
|
||||
* @return void
|
||||
* @throws SubmissionException
|
||||
*/
|
||||
private function Step11_SaveLanguages( Entry $entry ): void
|
||||
{
|
||||
foreach ( $this->request->input('languages', [] ) as $languageId ) {
|
||||
$language = Language::find( $languageId );
|
||||
if( !$language )
|
||||
throw new SubmissionException( "Language {$languageId} does not exist." );
|
||||
$entry->languages()->attach( $language->id );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function Step12a_PrepareGalleryImages( Entry $entry ): void
|
||||
{
|
||||
foreach ( $this->request->input('gallery', [] ) as $imagePath ) {
|
||||
EntryGallery::create([
|
||||
'entry_id' => $entry->id,
|
||||
'image' => $imagePath,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entry $entry
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function Step12b_MoveMainImage( Entry $entry ): void {
|
||||
$mainImage = $entry->main_image;
|
||||
$newPath = 'entries/main-images/' . basename($mainImage);
|
||||
|
||||
if( !Storage::disk('public')->move($mainImage, $newPath) )
|
||||
return;
|
||||
|
||||
$entry->update(['main_image' => $newPath]);
|
||||
}
|
||||
|
||||
private function Step12c_SaveGalleryImages( Entry $entry ): void
|
||||
{
|
||||
foreach ( $entry->gallery as $galleryItem ) {
|
||||
$newPath = 'entries/gallery-images/' . $entry->id . '/' . basename($galleryItem->image);
|
||||
|
||||
if( !Storage::disk('public')->move($galleryItem->image, $newPath) )
|
||||
continue;
|
||||
|
||||
$galleryItem->update(['image' => $newPath]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
25
app/Services/TemporaryFileService.php
Normal file
25
app/Services/TemporaryFileService.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class TemporaryFileService {
|
||||
|
||||
public const int NB_HOURS_FILES_KEPT = 6;
|
||||
|
||||
/**
|
||||
* Upload a file in the temporary path.
|
||||
*
|
||||
* @param UploadedFile|null $file
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function uploadFile(?UploadedFile $file ): string|bool {
|
||||
|
||||
if( !$file )
|
||||
return false;
|
||||
|
||||
return $file->store( 'temp', 'public' );
|
||||
}
|
||||
}
|
||||
36
app/Services/XenforoService.php
Normal file
36
app/Services/XenforoService.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class XenforoService {
|
||||
|
||||
private const array PERMISSIONS_KEPT = [ 'general', 'romhackplaza' ];
|
||||
private const int TTL_PERMISSIONS = 300;
|
||||
|
||||
public function getPermissions(int $userId, int $permissionCombinationId): array {
|
||||
|
||||
return Cache::remember("xf_permissions_{$userId}", self::TTL_PERMISSIONS, function() use($permissionCombinationId) {
|
||||
$row = \DB::connection('xenforo')
|
||||
->table('permission_combination')
|
||||
->where('permission_combination_id', $permissionCombinationId)
|
||||
->value('cache_value');
|
||||
|
||||
if( !$row )
|
||||
return [];
|
||||
|
||||
$data = json_decode($row, true);
|
||||
$data = array_intersect_key($data, array_flip(self::PERMISSIONS_KEPT));
|
||||
|
||||
return $data ?: [];
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function clearUserData(int $userId): void
|
||||
{
|
||||
Cache::forget("xf_permissions_{$userId}");
|
||||
}
|
||||
}
|
||||
19
app/Types/FSTypes.php
Normal file
19
app/Types/FSTypes.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Types;
|
||||
|
||||
/**
|
||||
* @phpstan-type FSFileData array{
|
||||
* name:string,
|
||||
* totalChunks:int,
|
||||
* rawFile?:mixed, // Must be used only in JS.
|
||||
* progressValue:int,
|
||||
* currentChunk:int,
|
||||
* done:bool,
|
||||
* error:mixed,
|
||||
* uuid:string
|
||||
* }
|
||||
*/
|
||||
interface FSTypes
|
||||
{
|
||||
}
|
||||
14
app/Types/SubmissionTypes.php
Normal file
14
app/Types/SubmissionTypes.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Types;
|
||||
|
||||
/**
|
||||
* @phpstan-type HashObject array{
|
||||
* filename:string,
|
||||
* hash_crc32:string,
|
||||
* hash_sha1:string,
|
||||
* verified:string
|
||||
* }
|
||||
*/
|
||||
interface SubmissionTypes
|
||||
{}
|
||||
30
app/View/Components/EntryMetaItem.php
Normal file
30
app/View/Components/EntryMetaItem.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class EntryMetaItem extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public string $value,
|
||||
public string $route = "#"
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.entry-meta-item');
|
||||
}
|
||||
}
|
||||
29
app/View/Components/EntrySectionTitle.php
Normal file
29
app/View/Components/EntrySectionTitle.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class EntrySectionTitle extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public string $icon = ''
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.entry-section-title');
|
||||
}
|
||||
}
|
||||
43
app/View/Components/ErrorBlock.php
Normal file
43
app/View/Components/ErrorBlock.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ErrorBlock extends Component
|
||||
{
|
||||
|
||||
private const array ERROR_TYPES = [
|
||||
'custom' => [
|
||||
'icon' => 'ban',
|
||||
'message' => '%s'
|
||||
],
|
||||
'page-not-allowed' => [
|
||||
'icon' => 'shield-ban',
|
||||
'message' => "You do not have permission to access this page.\nRequired permission: %s"
|
||||
]
|
||||
];
|
||||
|
||||
public array $errorArray;
|
||||
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $errorType,
|
||||
public string $message = ""
|
||||
)
|
||||
{
|
||||
$this->errorArray = self::ERROR_TYPES[$errorType] ?? self::ERROR_TYPES['custom'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.error-block');
|
||||
}
|
||||
}
|
||||
29
app/View/Components/FormErrorText.php
Normal file
29
app/View/Components/FormErrorText.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class FormErrorText extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $message,
|
||||
public bool $icon = true
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.form-error-text');
|
||||
}
|
||||
}
|
||||
30
app/View/Components/FormFieldTitle.php
Normal file
30
app/View/Components/FormFieldTitle.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class FormFieldTitle extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $helper = "",
|
||||
public bool $required = false,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.form-field-title');
|
||||
}
|
||||
}
|
||||
29
app/View/Components/FormGroupTitle.php
Normal file
29
app/View/Components/FormGroupTitle.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class FormGroupTitle extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public string $icon = ""
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.form-group-title');
|
||||
}
|
||||
}
|
||||
29
app/View/Components/GalleryField.php
Normal file
29
app/View/Components/GalleryField.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class GalleryField extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public array $oldPaths = [],
|
||||
public bool $required = true,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.gallery-field');
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user