Initial commit

This commit is contained in:
2026-05-20 18:25:15 +02:00
commit 95f0b4ff01
288 changed files with 90909 additions and 0 deletions

18
.editorconfig Normal file
View 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
View 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
View 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
View 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

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
ignore-scripts=true
audit=true

58
README.md Normal file
View 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
View 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
View 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;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class SubmissionException extends Exception
{
//
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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'],
};
}
}

View 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] ?? [];
}
}

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

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

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

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

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

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

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

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

View 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.',
];
}
}

View 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'
];
}
}

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

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

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

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Language extends Model
{
protected $fillable = [ 'name', 'slug' ];
}

View 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
View 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
View 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
View 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',
];
}
}

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

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

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

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

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

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

View 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
View 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
{
}

View 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
{}

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

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

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

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

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

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

View 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