This commit is contained in:
2024-05-23 16:54:06 +02:00
parent fc2b66528b
commit 3f26df05b5
26 changed files with 1193 additions and 0 deletions

8
app/HasThumbnail.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
namespace App;
interface HasThumbnail
{
public function getThumbnail() : string;
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreImageRequest;
use App\Http\Requests\UpdateImageRequest;
use App\Models\Image;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ImageController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreImageRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Image $image) : BinaryFileResponse
{
return response()->file(Storage::disk('images')->path($image->album->id . '/original/' . $image->id));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Image $image)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateImageRequest $request, Image $image)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Image $image)
{
//
}
}

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Drawer\Album;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\WithFileUploads;
class AddImage extends Component
{
use WithFileUploads;
public $media = [];
public function render()
{
return view('livewire.drawer.album.add-image');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Livewire\Drawer\Album;
use App\Models\Category;
use Carbon\Carbon;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Computed;
class Create extends Component
{
public Category $category;
#[Validate('required|min:3')]
public string $name;
#[Validate('required|date')]
public string $capture_date_string;
#[Computed]
public function captureDate() : Carbon {
return Carbon::parse($this->capture_date_string);
}
public function mount(Category $category) : void {
$this->category = $category;
}
public function save() : void {
$this->validate();
}
public function render()
{
return view('livewire.drawer.album.create');
}
}

22
app/Models/Image.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use App\HasThumbnail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Image extends Model implements HasThumbnail
{
use HasFactory;
public function album(): BelongsTo
{
return $this->belongsTo(Album::class);
}
public function getThumbnail() : string {
return route('image.show', $this);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Image;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ImagePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
//
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Image $image): bool
{
//
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
//
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Image $image): bool
{
//
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Image $image): bool
{
//
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Image $image): bool
{
//
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Image $image): bool
{
//
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\TelescopeApplicationServiceProvider;
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// Telescope::night();
$this->hideSensitiveRequestDetails();
$isLocal = $this->app->environment('local');
Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
return $isLocal ||
$entry->isReportableException() ||
$entry->isFailedRequest() ||
$entry->isFailedJob() ||
$entry->isScheduledTask() ||
$entry->hasMonitoredTag();
});
}
/**
* Prevent sensitive request details from being logged by Telescope.
*/
protected function hideSensitiveRequestDetails(): void
{
if ($this->app->environment('local')) {
return;
}
Telescope::hideRequestParameters(['_token']);
Telescope::hideRequestHeaders([
'cookie',
'x-csrf-token',
'x-xsrf-token',
]);
}
/**
* Register the Telescope gate.
*
* This gate determines who can access Telescope in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewTelescope', function ($user) {
return in_array($user->email, [
//
]);
});
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\View\Components\Form;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Upload extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public string $name = 'file',
public bool|int $multiple = false,
public bool|int $validate = true,
public bool|int $preview = true,
public bool|int $required = false,
public bool|int $disabled = false,
public array|string $accept = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'],
public string $size = '8MB',
public int $number = 10,
public string $label = '',
public string $sizeHuman = '',
public array|string $acceptHuman = [],
) {
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
// Set boolean values
if (! $this->multiple) {
$this->multiple = 0;
}
if (! $this->validate) {
$this->validate = 0;
}
if (! $this->preview) {
$this->preview = 0;
}
if (! $this->required) {
$this->required = 0;
}
if (! $this->disabled) {
$this->disabled = 0;
}
// Prepare accept files to JSON
if (is_string($this->accept)) {
$this->accept = explode(',', $this->accept);
}
$this->accept = array_map('trim', $this->accept);
$this->accept = array_filter($this->accept);
$this->accept = array_unique($this->accept);
$this->accept = array_values($this->accept);
$this->accept = array_map('strtolower', $this->accept);
$fileTypes = $this->accept;
$this->accept = json_encode($this->accept);
// Set size human for UI
$this->sizeHuman = $this->size;
// Prepare files types for UI
foreach ($fileTypes as $type) {
$new = explode('/', $type);
if (array_key_exists(1, $new)) {
$this->acceptHuman[] = ".{$new[1]}";
}
}
$this->acceptHuman = implode(', ', $this->acceptHuman);
return view('components.form.upload');
}
}

159
config/livewire.php Normal file
View File

@@ -0,0 +1,159 @@
<?php
return [
/*
|---------------------------------------------------------------------------
| Class Namespace
|---------------------------------------------------------------------------
|
| This value sets the root class namespace for Livewire component classes in
| your application. This value will change where component auto-discovery
| finds components. It's also referenced by the file creation commands.
|
*/
'class_namespace' => 'App\\Livewire',
/*
|---------------------------------------------------------------------------
| View Path
|---------------------------------------------------------------------------
|
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
| Layout
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
| In this case, the view returned by CreatePost will render into $slot.
|
*/
'layout' => 'components.layouts.app',
/*
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|
*/
'lazy_placeholder' => null,
/*
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
|
*/
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
],
/*
|---------------------------------------------------------------------------
| Render On Redirect
|---------------------------------------------------------------------------
|
| This value determines if Livewire will run a component's `render()` method
| after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
|
*/
'render_on_redirect' => false,
/*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => false,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
];

205
config/telescope.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
use Laravel\Telescope\Http\Middleware\Authorize;
use Laravel\Telescope\Watchers;
return [
/*
|--------------------------------------------------------------------------
| Telescope Master Switch
|--------------------------------------------------------------------------
|
| This option may be used to disable all Telescope watchers regardless
| of their individual configuration, which simply provides a single
| and convenient way to enable or disable Telescope data storage.
|
*/
'enabled' => env('TELESCOPE_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Telescope Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Telescope will be accessible from. If the
| setting is null, Telescope will reside under the same domain as the
| application. Otherwise, this value will be used as the subdomain.
|
*/
'domain' => env('TELESCOPE_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Telescope Path
|--------------------------------------------------------------------------
|
| This is the URI path where Telescope will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => env('TELESCOPE_PATH', 'telescope'),
/*
|--------------------------------------------------------------------------
| Telescope Storage Driver
|--------------------------------------------------------------------------
|
| This configuration options determines the storage driver that will
| be used to store Telescope's data. In addition, you may set any
| custom options as needed by the particular driver you choose.
|
*/
'driver' => env('TELESCOPE_DRIVER', 'database'),
'storage' => [
'database' => [
'connection' => env('DB_CONNECTION', 'mysql'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Telescope Queue
|--------------------------------------------------------------------------
|
| This configuration options determines the queue connection and queue
| which will be used to process ProcessPendingUpdate jobs. This can
| be changed if you would prefer to use a non-default connection.
|
*/
'queue' => [
'connection' => env('TELESCOPE_QUEUE_CONNECTION', null),
'queue' => env('TELESCOPE_QUEUE', null),
],
/*
|--------------------------------------------------------------------------
| Telescope Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will be assigned to every Telescope route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => [
'web',
Authorize::class,
],
/*
|--------------------------------------------------------------------------
| Allowed / Ignored Paths & Commands
|--------------------------------------------------------------------------
|
| The following array lists the URI paths and Artisan commands that will
| not be watched by Telescope. In addition to this list, some Laravel
| commands, like migrations and queue commands, are always ignored.
|
*/
'only_paths' => [
// 'api/*'
],
'ignore_paths' => [
'livewire*',
'nova-api*',
'pulse*',
],
'ignore_commands' => [
//
],
/*
|--------------------------------------------------------------------------
| Telescope Watchers
|--------------------------------------------------------------------------
|
| The following array lists the "watchers" that will be registered with
| Telescope. The watchers gather the application's profile data when
| a request or task is executed. Feel free to customize this list.
|
*/
'watchers' => [
Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
Watchers\CacheWatcher::class => [
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
'hidden' => [],
],
Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
Watchers\CommandWatcher::class => [
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
'ignore' => [],
],
Watchers\DumpWatcher::class => [
'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
],
Watchers\EventWatcher::class => [
'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
'ignore' => [],
],
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
Watchers\GateWatcher::class => [
'enabled' => env('TELESCOPE_GATE_WATCHER', true),
'ignore_abilities' => [],
'ignore_packages' => true,
'ignore_paths' => [],
],
Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
Watchers\LogWatcher::class => [
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
'level' => 'error',
],
Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
Watchers\ModelWatcher::class => [
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
'events' => ['eloquent.*'],
'hydrations' => true,
],
Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'ignore_packages' => true,
'ignore_paths' => [],
'slow' => 100,
],
Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
Watchers\RequestWatcher::class => [
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
'ignore_http_methods' => [],
'ignore_status_codes' => [],
],
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
],
];

View File

@@ -0,0 +1,47 @@
<?php
namespace Database\Factories;
use App\Models\Album;
use App\Models\Image;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Image>
*/
class ImageFactory extends Factory
{
/**
* Configure the model factory.
*/
public function configure(): static
{
return $this->afterCreating(function (Image $image) {
if($image->album->images->sortBy('id')->first()->id == $image->id) {
$image->isCover = true;
$image->save();
}
$height = rand(2000, 4000);
$width = rand(2000, 4000);
$image_content = Http::get("https://picsum.photos/{$width}/{$height}")->body();
Storage::disk('images')->put($image->album->id . '/original/' . $image->id, $image_content);
});
}
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'album_id' => Album::all()->random(1)->first()->id,
'isCover' => false,
];
}
}

View File

@@ -0,0 +1,70 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Get the migration connection name.
*/
public function getConnection(): ?string
{
return config('telescope.storage.database.connection');
}
/**
* Run the migrations.
*/
public function up(): void
{
$schema = Schema::connection($this->getConnection());
$schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence');
$table->uuid('uuid');
$table->uuid('batch_id');
$table->string('family_hash')->nullable();
$table->boolean('should_display_on_index')->default(true);
$table->string('type', 20);
$table->longText('content');
$table->dateTime('created_at')->nullable();
$table->unique('uuid');
$table->index('batch_id');
$table->index('family_hash');
$table->index('created_at');
$table->index(['type', 'should_display_on_index']);
});
$schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid');
$table->string('tag');
$table->primary(['entry_uuid', 'tag']);
$table->index('tag');
$table->foreign('entry_uuid')
->references('uuid')
->on('telescope_entries')
->onDelete('cascade');
});
$schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$schema = Schema::connection($this->getConnection());
$schema->dropIfExists('telescope_entries_tags');
$schema->dropIfExists('telescope_entries');
$schema->dropIfExists('telescope_monitoring');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use App\Models\Album;
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('images', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->boolean('isCover');
$table->foreignIdFor(Album::class);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('images');
}
};

View File

@@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Models\Image;
use Illuminate\Database\Seeder;
class ImageSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Image::factory()
->count(100)
->create();
}
}

8
public/vendor/telescope/app-dark.css vendored Normal file

File diff suppressed because one or more lines are too long

7
public/vendor/telescope/app.css vendored Normal file

File diff suppressed because one or more lines are too long

2
public/vendor/telescope/app.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
public/vendor/telescope/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,5 @@
{
"/app.js": "/app.js?id=7049e92a398e816f8cd53a915eaea592",
"/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9",
"/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf"
}

View File

@@ -0,0 +1,48 @@
@push('menu')
<x-menu-action tooltip="Einstellungen">
<svg class="w-5 h-5 mb-1 text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12.25V1m0 11.25a2.25 2.25 0 0 0 0 4.5m0-4.5a2.25 2.25 0 0 1 0 4.5M4 19v-2.25m6-13.5V1m0 2.25a2.25 2.25 0 0 0 0 4.5m0-4.5a2.25 2.25 0 0 1 0 4.5M10 19V7.75m6 4.5V1m0 11.25a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM16 19v-2"/>
</svg>
</x-menu-action>
<div class="flex items-center justify-center">
<x-drawer-trigger target="image-add" action="open">
<button data-tooltip-target="tooltip-new" type="button" class="inline-flex items-center justify-center w-10 h-10 font-medium bg-blue-600 rounded-full hover:bg-blue-700 group focus:ring-4 focus:ring-blue-300 focus:outline-none dark:focus:ring-blue-800">
<svg class="w-4 h-4 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 1v16M1 9h16"/>
</svg>
<span class="sr-only">Add Images</span>
</button>
</x-drawer-trigger>
</div>
<div id="tooltip-new" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
Add images
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
@endpush
<x-layout>
<x-hero-search></x-hero-search>
<h1 class="mb-4 mx-8 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white">
{{ $album->name }}
</h1>
<div class="m-8 flex flex-wrap flex-row gap-4">
@foreach ($album->images as $image)
<figure class="relative group rounded-lg cursor-pointer h-80 flex-grow overflow-hidden">
<img class="max-h-full min-w-full align-bottom object-cover"
src="{{ $image->getThumbnail() }}" alt="{{ $album->name }} Bild">
<div class="opacity-0 group-hover:opacity-40 absolute inset-0 w-full h-full bg-black flex items-center justify-center transition-opacity">
<svg class="w-1/2 h-1/2 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6Z"/>
<path stroke="currentColor" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
</svg>
</div>
</figure>
@endforeach
</div>
<x-drawer name="image-add" >
<x-slot:title>Neue Bilder zu {{ $album->name }} hinzufügen</x-slot:title>
<x-slot:content>
<livewire:drawer.album.addImage></livewire:drawer.album.addImage>
</x-slot:content>
</x-drawer>
</x-layout>

View File

@@ -0,0 +1,20 @@
@props([
'name' => 'undefined',
'label' => 'Undefined',
'type' => 'text',
'placeholder' => '',
])
<div class="mb-5">
<label for="{{ $name }}" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $label }}</label>
<input type="{{ $type }}" id="{{ $name }}"
@class([
'border text-sm rounded-lg block w-full p-2.5',
'bg-red-50 border-red-500 text-red-900 placeholder-red-700 focus:ring-red-500 dark:bg-gray-700 focus:border-red-500 dark:text-red-500 dark:placeholder-red-500 dark:border-red-500' => $errors->has($name),
'bg-gray-50 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500' => !$errors->has($name),
])
placeholder="{{ $placeholder }}" {{ $attributes }} />
@error($name)
<p class="mt-2 text-sm text-red-600 dark:text-red-500"><span class="font-medium">Oops!</span> {{ $message }}</p>
@enderror
</div>

View File

@@ -0,0 +1,123 @@
<div
class="relative xl:min-w-[1000px]"
wire:ignore
x-cloak
x-data="{
model: @entangle($attributes->whereStartsWith('wire:model')->first()),
isMultiple: {{ $multiple ? 'true' : 'false' }},
current: undefined,
currentList: [],
async URLtoFile(path) {
let url = `${window.appUrlStorage}/${path}`;
let name = url.split('/').pop();
const response = await fetch(url);
const data = await response.blob();
const metadata = {
name: name,
size: data.size,
type: data.type
};
let file = new File([data], name, metadata);
return {
source: file,
options: {
type: 'local',
metadata: {
name: name,
size: file.size,
type: file.type
}
}
}
}
}"
x-init="async () => {
let picture = model;
let files = [];
let exists = [];
if (model) {
if (isMultiple) {
currentList = model.map((picture) => `${window.appUrlStorage}/${picture}`);
await Promise.all(model.map(async (picture) => exists.push(await URLtoFile(picture))));
} else {
if (picture) {
exists.push(await URLtoFile(picture));
}
}
}
files = exists;
let modelName = '{{ $attributes->whereStartsWith('wire:model')->first() }}';
const pond = FilePond.create($refs.{{ $attributes->get('ref') ?? 'input' }}, { credits: false});
const uploadWatcher = () => {
$store.uploader.setState('{{ $name }}', pond.status);
}
pond.setOptions({
allowMultiple: {{ $multiple ? 'true' : 'false' }},
server: {
process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
@this.upload(modelName, file, load, error, progress)
},
revert: (filename, load) => {
@this.removeUpload(modelName, filename, load)
},
remove: (filename, load) => {
@this.removeFile(modelName, filename.name)
load();
},
},
allowImagePreview: {{ $preview ? 'true' : 'false' }},
styleItemPanelAspectRatio: '0.5625',
allowFileTypeValidation: {{ $validate ? 'true' : 'false' }},
acceptedFileTypes: {{ $accept ? $accept : 'null' }},
allowFileSizeValidation: {{ $validate ? 'true' : 'false' }},
maxFileSize: {!! $size ? "'" . $size . "'" : 'null' !!},
maxFiles: {{ $number ? $number : 'null' }},
required: {{ $required ? 'true' : 'false' }},
disabled: {{ $disabled ? 'true' : 'false' }},
onaddfilestart: uploadWatcher,
onprocessfile: uploadWatcher
});
pond.addFiles(files);
pond.on('addfile', (error, file) => {
if (error) {
console.log('Oh no');
return;
}
});
uploadWatcher();
}"
>
@if ($label)
<div class="flex items-center justify-between">
<label class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-100"
for="{{ $name }}"
>
{{ $label }}
@if ($required)
<span class="text-red-500" title="Required">*</span>
@endif
</label>
<div class="text-xs text-gray-400">Size max: {{ $sizeHuman }}</div>
</div>
@endif
<div class="flex items-center justify-between text-xs text-gray-400">
<div>Formats: {{ $acceptHuman }}</div>
<div>
{{ $multiple ? 'Multiple' : 'Single' }}
@if ($multiple)
<span>({{ $number }} files max)</span>
@endif
</div>
</div>
<div class="mt-5">
<input type="file"
x-ref="{{ $attributes->get('ref') ?? 'input' }}"
/>
</div>
@error('image')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>

View File

@@ -0,0 +1,21 @@
<form wire:submit="save" class="p-4">
<x-form.upload wire:model="media" name="media" label="Medien" multiple />
<button x-show="$store.uploader.states && $store.uploader.states.media == 3" disabled type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 inline-flex items-center">
<svg aria-hidden="true" role="status" class="inline w-4 h-4 me-3 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/>
</svg>
Bitte warten ...
</button>
<div x-show="$store.uploader.states && $store.uploader.states.media == 2" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">Info</span>
<div>
<span class="font-medium">Dateien mit Fehlern vorhanden!</span> Du kannst nicht speichern, bis die Fehler behoben sind.
</div>
</div>
<button x-show="$store.uploader.states && $store.uploader.states.media == 4" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Speichern</button>
</div>

View File

@@ -0,0 +1,14 @@
<div>
<form class="p-4" wire:submit="save">
<x-form.input name="name" label="Name" wire:model="name" required/>
<x-form.input name="capture_date_string" label="Aufnahmedatum" type="date" wire:model="capture_date_string" required />
<button type="submit"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Abbrechen</button>
<button type="submit"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Speichern</button>
</form>
</div>