Merge branch 'main' of git.digital-infinity.de:DigitalInfinity/NAI_Photobox

This commit is contained in:
Alexander Gabriel 2026-07-01 10:46:21 +02:00
commit 52d0d725f5
46 changed files with 3443 additions and 443 deletions

View File

@ -0,0 +1,35 @@
{
"name": "Fotobox",
"image": "mcr.microsoft.com/devcontainers/base:trixie",
"postCreateCommand": "sudo sh docker/init.sh",
"forwardPorts": [8000],
"portsAttributes": {
"8000": {
"label": "Laravel Dev Server",
"onAutoForward": "notify"
}
},
"customizations": {
"vscode": {
"extensions": [
"bmewburn.vscode-intelephense-client",
"onecentlin.laravel-blade",
"amiralizadeh9480.laravel-extra-intellisense",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"laravel.vscode-laravel",
"bradlc.vscode-tailwindcss"
],
"settings": {
"php.validate.executablePath": "/usr/bin/php",
"intelephense.environment.phpVersion": "8.5",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
}
}
}
}
}

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
vendor/
node_modules/
public/build/
.git/
.env
storage/framework/
storage/logs/

View File

@ -63,3 +63,5 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
PHOTO_RETENTION_HOURS=24

3
.gitignore vendored
View File

@ -26,3 +26,6 @@ _ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db
public/fonts/filament/filament/*/*.woff2
public/js/filament/**/*.js
public/*/filament/**/*.css

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM debian:trixie-backports
RUN apt-get update && apt-get install -y git curl zip unzip
RUN rm -rf /var/lib/apt/lists/*
# Node.js 22
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app

View File

@ -0,0 +1,28 @@
<?php
namespace App\Console\Commands;
use App\Models\PhotoJob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanupPhotos extends Command
{
protected $signature = 'photos:cleanup';
protected $description = 'Delete photos and their jobs older than the configured retention period';
public function handle(): void
{
$hours = config('fotobox.photo_retention_hours');
$cutoff = now()->subHours($hours);
$jobs = PhotoJob::where('created_at', '<', $cutoff)->get();
foreach ($jobs as $job) {
Storage::disk('public')->delete($job->image_path);
$job->delete();
}
$this->info("Deleted {$jobs->count()} photo(s) older than {$hours} hour(s).");
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\Photoprofiles;
use App\Filament\Resources\Photoprofiles\Pages\CreatePhotoprofile;
use App\Filament\Resources\Photoprofiles\Pages\EditPhotoprofile;
use App\Filament\Resources\Photoprofiles\Pages\ListPhotoprofiles;
use App\Filament\Resources\Photoprofiles\Schemas\PhotoprofileForm;
use App\Filament\Resources\Photoprofiles\Tables\PhotoprofilesTable;
use App\Models\Photoprofile;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class PhotoprofileResource extends Resource
{
protected static ?string $model = Photoprofile::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'name';
public static function form(Schema $schema): Schema
{
return PhotoprofileForm::configure($schema);
}
public static function table(Table $table): Table
{
return PhotoprofilesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListPhotoprofiles::route('/'),
'create' => CreatePhotoprofile::route('/create'),
'edit' => EditPhotoprofile::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Filament\Resources\Photoprofiles\Schemas;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class PhotoprofileForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Name')
->required(),
Checkbox::make('active')
->label('Aktiv'),
Textarea::make('commands')
->label('Befehle')
->required(),
]);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Filament\Resources\Photoprofiles\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class PhotoprofilesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('name')
->label('Name')
->searchable(),
IconColumn::make('active')
->label('Aktiv')
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\Users\Schemas;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class UserForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Name')
->required(),
TextInput::make('email')
->label('E-Mail')
->email()
->required()
->unique(ignoreRecord: true),
TextInput::make('password')
->label('Passwort')
->password()
->revealable()
->required(fn (string $operation) => $operation === 'create')
->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null)
->dehydrated(fn ($state) => filled($state))
->minLength(8),
Checkbox::make('is_admin')
->label('Administrator'),
]);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Filament\Resources\Users\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class UsersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Name')
->searchable()
->sortable(),
TextColumn::make('email')
->label('E-Mail')
->searchable()
->sortable(),
IconColumn::make('is_admin')
->label('Administrator'),
TextColumn::make('created_at')
->label('Erstellt')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Filament\Resources\Users;
use App\Filament\Resources\Users\Pages\CreateUser;
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Filament\Resources\Users\Schemas\UserForm;
use App\Filament\Resources\Users\Tables\UsersTable;
use App\Models\User;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUsers;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $navigationLabel = 'Benutzer';
protected static ?string $modelLabel = 'Benutzer';
protected static ?string $pluralModelLabel = 'Benutzer';
public static function form(Schema $schema): Schema
{
return UserForm::configure($schema);
}
public static function table(Table $table): Table
{
return UsersTable::configure($table);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function show()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
return redirect()->intended('/');
}
return back()->withErrors(['email' => 'Die eingegebenen Daten stimmen nicht überein.'])->onlyInput('email');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RestrictToSubnet
{
public function handle(Request $request, Closure $next, string $subnet = '192'): Response
{
if (!str_starts_with($request->ip(), $subnet) && !str_starts_with($request->ip(), "127")) {
abort(403);
}
return $next($request);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\PhotoJob;
use App\Models\Photoprofile;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Process;
@ -20,12 +21,15 @@ class ConvertToComic implements ShouldQueue
{
$absolutePath = Storage::disk('public')->path($this->photoJob->image_path);
$command = 'gmic ' . escapeshellarg($absolutePath) .
' simplelocalcontrast_p 16,2,0,1,1,1,1,1,1,1,1,0' .
' -o ' . escapeshellarg($absolutePath) .
' && gmic ' . escapeshellarg($absolutePath) .
' cl_comic 4,1,0,0,1,15,15,1,10,20,6,2,0,0,0,0,0,0,50,50' .
' -o ' . escapeshellarg($absolutePath);
$photoprofile = $this->photoJob->photoprofile
?? Photoprofile::where('active', 1)->first();
$photocommands = [];
foreach (explode(PHP_EOL, $photoprofile->commands) as $command) {
if (count($photocommands) > 0) $photocommands[] = ' && gmic ' . escapeshellarg($absolutePath) . ' ' . $command . ' -o ' . escapeshellarg($absolutePath);
else $photocommands[] = 'gmic ' . escapeshellarg($absolutePath) . ' ' . $command . ' -o ' . escapeshellarg($absolutePath);
}
$command = implode($photocommands);
$result = Process::timeout(0)->run($command);
$this->photoJob->update([

View File

@ -4,6 +4,7 @@ namespace App\Livewire;
use App\Jobs\ConvertToComic;
use App\Models\PhotoJob;
use App\Models\Photoprofile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Component;
@ -11,6 +12,12 @@ use Livewire\Component;
class CameraCapture extends Component
{
public string $status = '';
public ?int $photoprofileId = null;
public function mount(): void
{
$this->photoprofileId = Photoprofile::where('active', 1)->value('id');
}
public function delete(int $jobId): void
{
@ -39,6 +46,7 @@ class CameraCapture extends Component
$job = PhotoJob::create([
'image_path' => $filename,
'status' => 'processing',
'photoprofile_id' => $this->photoprofileId,
]);
ConvertToComic::dispatch($job);
@ -55,7 +63,8 @@ class CameraCapture extends Component
]);
$hasProcessing = $recentPhotos->contains('status', 'processing');
$photoprofiles = Photoprofile::where('active', 1)->pluck('name', 'id');
return view('livewire.camera-capture', compact('recentPhotos', 'hasProcessing'));
return view('livewire.camera-capture', compact('recentPhotos', 'hasProcessing', 'photoprofiles'));
}
}

View File

@ -3,8 +3,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\Photoprofile;
class PhotoJob extends Model
{
protected $fillable = ['image_path', 'status'];
protected $fillable = ['image_path', 'status', 'photoprofile_id'];
public function photoprofile()
{
return $this->belongsTo(Photoprofile::class);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Fillable('name', 'commands')]
class Photoprofile extends Model
{
//
}

View File

@ -2,31 +2,33 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
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'])]
#[Fillable(['name', 'email', 'password', 'is_admin'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
class User extends Authenticatable implements FilamentUser
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
public function canAccessPanel(Panel $panel): bool
{
return $this->is_admin;
}
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_admin' => 'boolean',
];
}
}

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 AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->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

@ -1,7 +1,6 @@
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
];

View File

@ -7,6 +7,7 @@
"license": "MIT",
"require": {
"php": "^8.3",
"filament/filament": "^5.0",
"laravel/framework": "^13.8",
"laravel/tinker": "^3.0",
"livewire/livewire": "^4.3"
@ -52,7 +53,8 @@
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
"@php artisan package:discover --ansi",
"@php artisan filament:upgrade"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"

2366
composer.lock generated

File diff suppressed because it is too large Load Diff

137
config/filament.php Normal file
View File

@ -0,0 +1,137 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Broadcasting
|--------------------------------------------------------------------------
|
| By uncommenting the Laravel Echo configuration, you may connect Filament
| to any Pusher-compatible websockets server.
|
| This will allow your users to receive real-time notifications.
|
*/
'broadcasting' => [
// 'echo' => [
// 'broadcaster' => 'pusher',
// 'key' => env('VITE_PUSHER_APP_KEY'),
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
// 'wsHost' => env('VITE_PUSHER_HOST'),
// 'wsPort' => env('VITE_PUSHER_PORT'),
// 'wssPort' => env('VITE_PUSHER_PORT'),
// 'authEndpoint' => '/broadcasting/auth',
// 'disableStats' => true,
// 'encrypted' => true,
// 'forceTLS' => env('VITE_PUSHER_SCHEME', 'https') === 'https',
// ],
],
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| This is the storage disk Filament will use to store files. You may use
| any of the disks defined in the `config/filesystems.php`.
|
*/
'default_filesystem_disk' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Temporary File URL Expiry
|--------------------------------------------------------------------------
|
| When Filament generates temporary URLs for previewing private files
| (file uploads, image columns, image entries, rich editor attachments,
| etc.), this value controls how many minutes those URLs remain valid.
|
| The generated URL's expiry is rounded up to the end of the hour it
| falls in, so the effective lifetime will be between this value and
| this value plus up to 60 minutes.
|
*/
'temporary_file_url_expiry_minutes' => 30,
/*
|--------------------------------------------------------------------------
| Assets Path
|--------------------------------------------------------------------------
|
| This is the directory where Filament's assets will be published to. It
| is relative to the `public` directory of your Laravel application.
|
| After changing the path, you should run `php artisan filament:assets`.
|
*/
'assets_path' => null,
/*
|--------------------------------------------------------------------------
| Cache Path
|--------------------------------------------------------------------------
|
| This is the directory that Filament will use to store cache files that
| are used to optimize the registration of components.
|
| After changing the path, you should run `php artisan filament:cache-components`.
|
*/
'cache_path' => base_path('bootstrap/cache/filament'),
/*
|--------------------------------------------------------------------------
| Livewire Loading Delay
|--------------------------------------------------------------------------
|
| This sets the delay before loading indicators appear.
|
| Setting this to 'none' makes indicators appear immediately, which can be
| desirable for high-latency connections. Setting it to 'default' applies
| Livewire's standard 200ms delay.
|
*/
'livewire_loading_delay' => 'default',
/*
|--------------------------------------------------------------------------
| File Generation
|--------------------------------------------------------------------------
|
| Artisan commands that generate files can be configured here by setting
| configuration flags that will impact their location or content.
|
| Often, this is useful to preserve file generation behavior from a
| previous version of Filament, to ensure consistency between older and
| newer generated files. These flags are often documented in the upgrade
| guide for the version of Filament you are upgrading to.
|
*/
'file_generation' => [
'flags' => [],
],
/*
|--------------------------------------------------------------------------
| System Route Prefix
|--------------------------------------------------------------------------
|
| This is the prefix used for the system routes that Filament registers,
| such as the routes for downloading exports and failed import rows.
|
*/
'system_route_prefix' => 'filament',
];

17
config/fotobox.php Normal file
View File

@ -0,0 +1,17 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Photo Retention
|--------------------------------------------------------------------------
|
| Photos and their associated jobs older than this many hours will be
| automatically deleted by the photos:cleanup scheduled command.
|
*/
'photo_retention_hours' => (int) env('PHOTO_RETENTION_HOURS', 24),
];

View File

@ -30,6 +30,7 @@ class UserFactory extends Factory
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
'is_admin' => false,
];
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('photoprofiles', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->boolean('active')->default(1);
$table->string("name");
$table->string("commands");
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('photoprofiles');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('photo_jobs', function (Blueprint $table) {
$table->foreignId('photoprofile_id')->nullable()->constrained('photoprofiles')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('photo_jobs', function (Blueprint $table) {
$table->dropForeignIdFor(\App\Models\Photoprofile::class);
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false)->after('remember_token');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};

View File

@ -2,7 +2,6 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
@ -10,16 +9,11 @@ class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
$this->call([
UserSeeder::class,
PhotoprofileSeeder::class,
]);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Database\Seeders;
use App\Models\Photoprofile;
use Illuminate\Database\Seeder;
class PhotoprofileSeeder extends Seeder
{
public function run(): void
{
$profiles = [
[
'name' => 'Comic',
'active' => true,
'commands' => 'simplelocalcontrast_p 16,2,0,1,1,1,1,1,1,1,1,0
cl_comic 4,1,0,0,1,15,15,1,10,20,6,2,0,0,0,0,0,0,50,50',
],
[
'name' => 'Edges',
'active' => true,
'commands' => 'fx_canny 5,0.05,0.15,1',
],
[
'name' => 'Tron',
'active' => true,
'commands' => 'samj_chalkitup 5,2.5,1.5,50,1,5,5,0,0,7,0.8,1.9,7,0',
],
[
'name' => 'Warhol',
'active' => true,
'commands' => 'warhol 3,3,2,40',
],
[
'name' => 'SW-Comic',
'active' => true,
'commands' => 'simplelocalcontrast_p 16,2,0,1,1,1,1,1,1,1,1,0
cl_lineart 0,0,2,1,15,15,1,0,6,2,2,0,0,0,50,50',
],
[
'name' => 'Neon',
'active' => true,
'commands' => 'fx_gradient2rgb 0.58,0,5.8,0,0',
],
[
'name' => 'Neon 2',
'active' => true,
'commands' => 'fx_gcd_canny 1,0.05,0.15',
]
];
foreach ($profiles as $profile) {
Photoprofile::firstOrCreate(['name' => $profile['name']], $profile);
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run(): void
{
User::firstOrCreate(
['email' => 'admin@fotobox.local'],
[
'name' => 'Admin',
'password' => Hash::make('admin'),
'is_admin' => true,
]
);
User::firstOrCreate(
['email' => 'user@fotobox.local'],
[
'name' => 'User',
'password' => Hash::make('user'),
'is_admin' => false,
]
);
}
}

39
docker-compose.yml Normal file
View File

@ -0,0 +1,39 @@
services:
init:
build: .
command: sh docker/init.sh
env_file: .env
volumes:
- .:/app
app:
build: .
command: php artisan serve --host=0.0.0.0 --port=8000
env_file: .env
ports:
- "8000:8000"
volumes:
- .:/app
depends_on:
init:
condition: service_completed_successfully
scheduler:
build: .
command: php artisan schedule:work
env_file: .env
volumes:
- .:/app
depends_on:
init:
condition: service_completed_successfully
worker:
build: .
command: php artisan queue:work --tries=3
env_file: .env
volumes:
- .:/app
depends_on:
init:
condition: service_completed_successfully

39
docker/init.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/sh
set -e
if [ "$(whoami)" != "root" ]; then
SUDO=sudo
fi
${SUDO} apt-get update
${SUDO} apt-get -y install lsb-release ca-certificates curl
${SUDO} curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
${SUDO} dpkg -i /tmp/debsuryorg-archive-keyring.deb
${SUDO} sh -c 'echo "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
${SUDO} apt-get update
apt-get update
apt-get -y install php8.5 php8.5-cli php8.5-zip php8.5-sqlite3 php8.5-xml php8.5-pgsql php8.5-bcmath php8.5-intl php8.5-mbstring sqlite3 composer npm
curl -fsSL -o gmic.deb "https://gmic.eu/get_file.php?file=linux/gmic_3.7.6_debian13_trixie_amd64.deb"
apt-get -y install ./gmic.deb
rm gmic.deb
echo "==> Installing Composer dependencies..."
composer install --no-interaction --prefer-dist --optimize-autoloader
echo "==> Installing npm dependencies..."
npm install
echo "==> Building frontend assets..."
npm run build
# Create SQLite database file if it doesn't exist yet
[ -f database/database.sqlite ] || touch database/database.sqlite
echo "==> Running database migrations..."
php artisan migrate --force
echo "==> Linking storage..."
php artisan storage:link --force 2>/dev/null || true
echo "==> Init complete."

352
package-lock.json generated
View File

@ -16,21 +16,21 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
@ -39,9 +39,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@ -100,14 +100,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.3"
},
"funding": {
"type": "github",
@ -119,9 +119,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.132.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"version": "0.137.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
"integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@ -129,9 +129,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
"integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
"cpu": [
"arm64"
],
@ -146,9 +146,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
"integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
"cpu": [
"arm64"
],
@ -163,9 +163,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
"integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
"cpu": [
"x64"
],
@ -180,9 +180,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
"integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
"cpu": [
"x64"
],
@ -197,9 +197,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
"integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
"cpu": [
"arm"
],
@ -214,9 +214,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
"integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
"cpu": [
"arm64"
],
@ -231,9 +231,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
"integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
"cpu": [
"arm64"
],
@ -248,9 +248,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
"integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
"cpu": [
"ppc64"
],
@ -265,9 +265,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
"integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
"cpu": [
"s390x"
],
@ -282,9 +282,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
"integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
"cpu": [
"x64"
],
@ -299,9 +299,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
"integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
"cpu": [
"x64"
],
@ -316,9 +316,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
"integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
"cpu": [
"arm64"
],
@ -333,9 +333,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
"integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
"cpu": [
"wasm32"
],
@ -343,18 +343,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "^1.1.6"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
"integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
"cpu": [
"arm64"
],
@ -369,9 +369,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
"integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
"cpu": [
"x64"
],
@ -393,49 +393,49 @@
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
"integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
"integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.21.0",
"jiti": "^2.6.1",
"enhanced-resolve": "5.21.6",
"jiti": "^2.7.0",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.3.0"
"tailwindcss": "4.3.1"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
"integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
"integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.3.0",
"@tailwindcss/oxide-darwin-arm64": "4.3.0",
"@tailwindcss/oxide-darwin-x64": "4.3.0",
"@tailwindcss/oxide-freebsd-x64": "4.3.0",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
"@tailwindcss/oxide-linux-x64-musl": "4.3.0",
"@tailwindcss/oxide-wasm32-wasi": "4.3.0",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
"@tailwindcss/oxide-android-arm64": "4.3.1",
"@tailwindcss/oxide-darwin-arm64": "4.3.1",
"@tailwindcss/oxide-darwin-x64": "4.3.1",
"@tailwindcss/oxide-freebsd-x64": "4.3.1",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
"@tailwindcss/oxide-linux-x64-musl": "4.3.1",
"@tailwindcss/oxide-wasm32-wasi": "4.3.1",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
"integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
"integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
"cpu": [
"arm64"
],
@ -450,9 +450,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
"integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
"integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
"cpu": [
"arm64"
],
@ -467,9 +467,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
"integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
"integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
"cpu": [
"x64"
],
@ -484,9 +484,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
"integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
"integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
"cpu": [
"x64"
],
@ -501,9 +501,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
"integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
"integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
"cpu": [
"arm"
],
@ -518,9 +518,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
"integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
"integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
"cpu": [
"arm64"
],
@ -535,9 +535,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
"integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
"integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
"cpu": [
"arm64"
],
@ -552,9 +552,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
"integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
"integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
"cpu": [
"x64"
],
@ -569,9 +569,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
"integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
"integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
"cpu": [
"x64"
],
@ -586,9 +586,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
"integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
"integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@ -608,7 +608,7 @@
"@emnapi/runtime": "^1.10.0",
"@emnapi/wasi-threads": "^1.2.1",
"@napi-rs/wasm-runtime": "^1.1.4",
"@tybys/wasm-util": "^0.10.1",
"@tybys/wasm-util": "^0.10.2",
"tslib": "^2.8.1"
},
"engines": {
@ -616,9 +616,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
"integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
"integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
"cpu": [
"arm64"
],
@ -633,9 +633,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
"integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
"integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
"cpu": [
"x64"
],
@ -650,24 +650,24 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz",
"integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
"integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.3.0",
"@tailwindcss/oxide": "4.3.0",
"tailwindcss": "4.3.0"
"@tailwindcss/node": "4.3.1",
"@tailwindcss/oxide": "4.3.1",
"tailwindcss": "4.3.1"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
@ -772,15 +772,15 @@
"license": "MIT"
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz",
"integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "4.1.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.3",
"shell-quote": "1.8.4",
"supports-color": "8.1.1",
"tree-kill": "1.2.2",
"yargs": "17.7.2"
@ -828,9 +828,9 @@
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
"integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
"version": "5.21.6",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1253,9 +1253,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"dev": true,
"funding": [
{
@ -1473,13 +1473,13 @@
"license": "ISC"
},
"node_modules/rolldown": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
"integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.132.0",
"@oxc-project/types": "=0.137.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@ -1489,21 +1489,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.2",
"@rolldown/binding-darwin-arm64": "1.0.2",
"@rolldown/binding-darwin-x64": "1.0.2",
"@rolldown/binding-freebsd-x64": "1.0.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
"@rolldown/binding-linux-arm64-musl": "1.0.2",
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
"@rolldown/binding-linux-x64-gnu": "1.0.2",
"@rolldown/binding-linux-x64-musl": "1.0.2",
"@rolldown/binding-openharmony-arm64": "1.0.2",
"@rolldown/binding-wasm32-wasi": "1.0.2",
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
"@rolldown/binding-win32-x64-msvc": "1.0.2"
"@rolldown/binding-android-arm64": "1.1.3",
"@rolldown/binding-darwin-arm64": "1.1.3",
"@rolldown/binding-darwin-x64": "1.1.3",
"@rolldown/binding-freebsd-x64": "1.1.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
"@rolldown/binding-linux-arm64-gnu": "1.1.3",
"@rolldown/binding-linux-arm64-musl": "1.1.3",
"@rolldown/binding-linux-ppc64-gnu": "1.1.3",
"@rolldown/binding-linux-s390x-gnu": "1.1.3",
"@rolldown/binding-linux-x64-gnu": "1.1.3",
"@rolldown/binding-linux-x64-musl": "1.1.3",
"@rolldown/binding-openharmony-arm64": "1.1.3",
"@rolldown/binding-wasm32-wasi": "1.1.3",
"@rolldown/binding-win32-arm64-msvc": "1.1.3",
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/rxjs": {
@ -1523,9 +1523,9 @@
"license": "ISC"
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -1588,9 +1588,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
"integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
"dev": true,
"license": "MIT"
},
@ -1609,9 +1609,9 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1643,17 +1643,17 @@
"license": "0BSD"
},
"node_modules/vite": {
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
"integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.15",
"rolldown": "1.0.2",
"tinyglobby": "^0.2.16"
"rolldown": "~1.1.2",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@ -1669,7 +1669,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.18",
"@vitejs/devtools": "^0.3.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fotobox Anmelden</title>
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
@vite(['resources/css/app.css', 'resources/js/app.js'])
@endif
</head>
<body class="bg-gray-950 text-white min-h-screen flex flex-col items-center justify-center p-6">
<h1 class="text-3xl font-bold mb-8 tracking-wide">Fotobox</h1>
<form method="POST" action="{{ route('login') }}" class="w-full max-w-sm flex flex-col gap-4">
@csrf
<div class="flex flex-col gap-1">
<label for="email" class="text-sm text-gray-400">E-Mail</label>
<input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus
class="rounded-xl bg-white/10 border border-white/20 px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-white/40">
@error('email')
<span class="text-sm text-red-400">{{ $message }}</span>
@enderror
</div>
<div class="flex flex-col gap-1">
<label for="password" class="text-sm text-gray-400">Passwort</label>
<input id="password" type="password" name="password" required
class="rounded-xl bg-white/10 border border-white/20 px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-white/40">
</div>
<label class="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
<input type="checkbox" name="remember" class="rounded">
Angemeldet bleiben
</label>
<button type="submit"
class="mt-2 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150">
Anmelden
</button>
</form>
</body>
</html>

View File

@ -9,11 +9,13 @@
@endif
@livewireStyles
</head>
<body class="bg-gray-950 text-white min-h-screen flex flex-col items-center justify-center p-6">
<body class="bg-gray-950 text-white h-screen flex flex-col items-center p-6 overflow-hidden">
<h1 class="text-3xl font-bold mb-8 tracking-wide">Fotobox</h1>
<h1 class="shrink-0 text-3xl font-bold mb-8 tracking-wide">Fotobox</h1>
<div class="flex-1 min-h-0 w-full flex flex-col items-center">
@livewire('camera-capture')
</div>
@livewireScripts
</body>

View File

@ -1,10 +1,11 @@
<div class="flex flex-col items-center w-full"
<div class="flex flex-col items-center w-full h-full"
x-data="{ lightbox: null }"
x-on:keydown.escape.window="lightbox = null">
<div class="relative w-full max-w-7xl">
<div class="flex-1 min-h-0 w-full flex justify-center">
<div class="relative h-full" style="aspect-ratio: 16/9; max-width: 100%;">
<video id="video" autoplay playsinline
class="w-full rounded-2xl shadow-2xl bg-gray-900 aspect-video object-cover" style="transform: scaleX(-1);"></video>
class="w-full h-full rounded-2xl shadow-2xl bg-gray-900" style="transform: scaleX(-1);"></video>
<canvas id="canvas" class="hidden"></canvas>
@ -14,19 +15,29 @@
<span id="countdown-num" class="text-white font-bold drop-shadow-lg" style="font-size: 8rem; line-height: 1;"></span>
</div>
</div>
</div>
<div class="shrink-0 mt-6 w-full max-w-xs">
<select wire:model="photoprofileId"
class="w-full rounded-full bg-black/50 text-white border border-white/20 px-5 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-white/40 appearance-none text-center">
@foreach ($photoprofiles as $id => $name)
<option value="{{ $id }}">{{ $name }}</option>
@endforeach
</select>
</div>
<button id="capture-btn"
wire:loading.attr="disabled"
@disabled($hasProcessing)
class="mt-8 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
class="shrink-0 mt-8 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
<span wire:loading.remove>{{ $hasProcessing ? 'Erstelle Bild...' : 'Foto aufnehmen' }}</span>
<span wire:loading>Saving…</span>
</button>
<div class="mt-4 text-sm text-gray-400 h-6">{{ $status }}</div>
<div class="shrink-0 mt-4 text-sm text-gray-400 h-6">{{ $status }}</div>
@if ($recentPhotos->isNotEmpty())
<div class="mt-10 w-full max-w-2xl">
<div class="shrink-0 mt-10 w-full max-w-2xl">
<h2 class="text-sm font-medium text-gray-400 mb-3 tracking-wide uppercase">Recent Photos</h2>
<div class="grid grid-cols-5 gap-2" @if($hasProcessing) wire:poll.2000ms @endif>
@foreach ($recentPhotos as $photo)
@ -95,7 +106,10 @@
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 4096 }, height: { ideal: 2160 } },
audio: false,
});
video.srcObject = stream;
} catch {
$wire.set('status', 'Camera access denied or unavailable.');
@ -131,7 +145,7 @@
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
canvas.getContext('2d').drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
const imageData = canvas.toDataURL('image/jpeg', 0.9);
await $wire.capture(imageData);

View File

@ -2,7 +2,10 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::command('photos:cleanup')->hourly();

View File

@ -1,8 +1,16 @@
<?php
use App\Http\Controllers\LoginController;
use App\Http\Controllers\PhotoController;
use Illuminate\Support\Facades\Route;
Route::get('/', [PhotoController::class, 'index'])
->middleware(\App\Http\Middleware\RestrictToSubnet::class)
->name('camera');
Route::middleware('guest')->group(function () {
Route::get('/login', [LoginController::class, 'show'])->name('login');
Route::post('/login', [LoginController::class, 'login']);
});
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::middleware('auth')->group(function () {
Route::get('/', [PhotoController::class, 'index'])->name('camera');
});