mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2025-12-13 11:46:47 +00:00
🛠️ Refactor service components: Add dynamic type filters, restructure landing page UI, and introduce ServiceForm for improved form handling and validations
This commit is contained in:
@@ -4,8 +4,49 @@ namespace App\Enums;
|
||||
|
||||
enum SelfHostedServiceType: string
|
||||
{
|
||||
case Mempool = 'mempool';
|
||||
case LNbits = 'lnbits';
|
||||
case Alby = 'alby';
|
||||
case BtcpayServer = 'btcpay_server';
|
||||
case ElectrumFulcrumServer = 'electrum_fulcrum_server';
|
||||
case LNbits = 'lnbits';
|
||||
case LnbitsServer = 'lnbits_server';
|
||||
case Mempool = 'mempool';
|
||||
case NostrBlossomServer = 'nostr_blossom_server';
|
||||
case NostrClient = 'nostr_client';
|
||||
case NostrRelayServer = 'nostr_relay_server';
|
||||
case PkarrDnsServer = 'pkarr_dns_server';
|
||||
case Other = 'other';
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Mempool => 'blue',
|
||||
self::LNbits => 'purple',
|
||||
self::Alby => 'amber',
|
||||
self::ElectrumFulcrumServer => 'cyan',
|
||||
self::BtcpayServer => 'green',
|
||||
self::LnbitsServer => 'violet',
|
||||
self::NostrRelayServer => 'fuchsia',
|
||||
self::NostrClient => 'pink',
|
||||
self::NostrBlossomServer => 'rose',
|
||||
self::PkarrDnsServer => 'orange',
|
||||
self::Other => 'zinc',
|
||||
};
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Mempool => 'Mempool',
|
||||
self::LNbits => 'LNbits',
|
||||
self::Alby => 'Alby',
|
||||
self::ElectrumFulcrumServer => 'Electrum/Fulcrum Server',
|
||||
self::BtcpayServer => 'BTCPay Server',
|
||||
self::LnbitsServer => 'LNbits Server',
|
||||
self::NostrRelayServer => 'Nostr Relay',
|
||||
self::NostrClient => 'Nostr Client',
|
||||
self::NostrBlossomServer => 'Nostr Blossom',
|
||||
self::PkarrDnsServer => 'Pkarr DNS Server',
|
||||
self::Other => 'Other',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
119
app/Livewire/Forms/ServiceForm.php
Normal file
119
app/Livewire/Forms/ServiceForm.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Enums\SelfHostedServiceType;
|
||||
use App\Models\SelfHostedService;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class ServiceForm extends Form
|
||||
{
|
||||
public ?SelfHostedService $service = null;
|
||||
|
||||
#[Validate('required|string|max:255')]
|
||||
public string $name = '';
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $intro = null;
|
||||
|
||||
#[Validate('nullable|url|max:255')]
|
||||
public ?string $url_clearnet = null;
|
||||
|
||||
#[Validate('nullable|string|max:255')]
|
||||
public ?string $url_onion = null;
|
||||
|
||||
#[Validate('nullable|string|max:255')]
|
||||
public ?string $url_i2p = null;
|
||||
|
||||
#[Validate('nullable|string|max:255')]
|
||||
public ?string $url_pkdns = null;
|
||||
|
||||
#[Validate('required')]
|
||||
public ?string $type = null;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $contact = null;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $anonymous = false;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => [
|
||||
'required',
|
||||
'in:' . collect(SelfHostedServiceType::cases())->map(fn($c) => $c->value)->implode(',')
|
||||
],
|
||||
'intro' => ['nullable', 'string'],
|
||||
'url_clearnet' => ['nullable', 'url', 'max:255'],
|
||||
'url_onion' => ['nullable', 'string', 'max:255'],
|
||||
'url_i2p' => ['nullable', 'string', 'max:255'],
|
||||
'url_pkdns' => ['nullable', 'string', 'max:255'],
|
||||
'contact' => ['nullable', 'string'],
|
||||
'anonymous' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function setService(SelfHostedService $service): void
|
||||
{
|
||||
$this->service = $service;
|
||||
|
||||
$this->name = $service->name;
|
||||
$this->intro = $service->intro;
|
||||
$this->url_clearnet = $service->url_clearnet;
|
||||
$this->url_onion = $service->url_onion;
|
||||
$this->url_i2p = $service->url_i2p;
|
||||
$this->url_pkdns = $service->url_pkdns;
|
||||
$this->type = $service->type?->value;
|
||||
$this->contact = $service->contact;
|
||||
$this->anonymous = is_null($service->created_by);
|
||||
}
|
||||
|
||||
public function store(): SelfHostedService
|
||||
{
|
||||
$this->validate();
|
||||
$this->validateAtLeastOneUrl();
|
||||
|
||||
return SelfHostedService::create([
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'intro' => $this->intro,
|
||||
'url_clearnet' => $this->url_clearnet,
|
||||
'url_onion' => $this->url_onion,
|
||||
'url_i2p' => $this->url_i2p,
|
||||
'url_pkdns' => $this->url_pkdns,
|
||||
'contact' => $this->contact,
|
||||
'created_by' => $this->anonymous ? null : auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->validate();
|
||||
$this->validateAtLeastOneUrl();
|
||||
|
||||
$this->service->update([
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'intro' => $this->intro,
|
||||
'url_clearnet' => $this->url_clearnet,
|
||||
'url_onion' => $this->url_onion,
|
||||
'url_i2p' => $this->url_i2p,
|
||||
'url_pkdns' => $this->url_pkdns,
|
||||
'contact' => $this->contact,
|
||||
'created_by' => $this->anonymous ? null : ($this->service->created_by ?? auth()->id()),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function validateAtLeastOneUrl(): void
|
||||
{
|
||||
if (empty($this->url_clearnet) && empty($this->url_onion) && empty($this->url_i2p) && empty($this->url_pkdns)) {
|
||||
$this->addError('url_clearnet', __('Mindestens eine URL muss angegeben werden.'));
|
||||
throw new \Illuminate\Validation\ValidationException(
|
||||
\Illuminate\Support\Facades\Validator::make([], [])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class User extends Authenticatable implements CipherSweetEncrypted
|
||||
->addOptionalTextField('node_id')
|
||||
->addOptionalTextField('email')
|
||||
->addOptionalTextField('paynym')
|
||||
->addJsonField('lnbits', $map)
|
||||
->addNullableJsonField('lnbits', $map, strict: false)
|
||||
->addBlindIndex('public_key', new BlindIndex('public_key_index'))
|
||||
->addBlindIndex('lightning_address', new BlindIndex('lightning_address_index'))
|
||||
->addBlindIndex('lnurl', new BlindIndex('lnurl_index'))
|
||||
|
||||
@@ -19,16 +19,16 @@ class SelfHostedServiceFactory extends Factory
|
||||
$name = $this->faker->unique()->company();
|
||||
|
||||
return [
|
||||
'created_by' => User::factory(),
|
||||
'created_by' => $this->faker->optional()->numberBetween(1,9),
|
||||
'name' => $name,
|
||||
'slug' => str($name)->slug(),
|
||||
'intro' => $this->faker->optional()->paragraph(),
|
||||
'url_clearnet' => $this->faker->optional()->url(),
|
||||
'url_onion' => null,
|
||||
'url_i2p' => null,
|
||||
'url_pkdns' => null,
|
||||
'url_onion' => $this->faker->optional()->url(),
|
||||
'url_i2p' => $this->faker->optional()->url(),
|
||||
'url_pkdns' => $this->faker->optional()->url(),
|
||||
'type' => $this->faker->randomElement(SelfHostedServiceType::cases())->value,
|
||||
'contact_url' => $this->faker->optional()->url(),
|
||||
'contact' => $this->faker->optional()->url(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\SelfHostedService;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
@@ -13,9 +13,6 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
SelfHostedService::factory(10)->create();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use App\Attributes\SeoDataAttribute;
|
||||
use App\Enums\SelfHostedServiceType;
|
||||
use App\Models\SelfHostedService;
|
||||
use App\Livewire\Forms\ServiceForm;
|
||||
use App\Traits\SeoTrait;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
@@ -11,85 +11,30 @@ use Livewire\WithFileUploads;
|
||||
new
|
||||
#[SeoDataAttribute(key: 'services_create')]
|
||||
class extends Component {
|
||||
use WithFileUploads;
|
||||
use SeoTrait;
|
||||
|
||||
#[Validate('image|max:10240')] // 10MB
|
||||
public $logo;
|
||||
public string $country = 'de';
|
||||
public ServiceForm $form;
|
||||
|
||||
public string $name = '';
|
||||
public ?string $intro = null;
|
||||
public ?string $url_clearnet = null;
|
||||
public ?string $url_onion = null;
|
||||
public ?string $url_i2p = null;
|
||||
public ?string $url_pkdns = null;
|
||||
public ?string $type = null;
|
||||
public ?string $contact = null;
|
||||
public bool $anonymous = false;
|
||||
|
||||
protected function rules(): array
|
||||
public function mount(): void
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => [
|
||||
'required', 'in:'.collect(SelfHostedServiceType::cases())->map(fn($c) => $c->value)->implode(',')
|
||||
],
|
||||
'intro' => ['nullable', 'string'],
|
||||
'url_clearnet' => ['nullable', 'url', 'max:255'],
|
||||
'url_onion' => ['nullable', 'string', 'max:255'],
|
||||
'url_i2p' => ['nullable', 'string', 'max:255'],
|
||||
'url_pkdns' => ['nullable', 'string', 'max:255'],
|
||||
'contact' => ['nullable', 'string'],
|
||||
'anonymous' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function validateAtLeastOneUrl(): void
|
||||
{
|
||||
if (empty($this->url_clearnet) && empty($this->url_onion) && empty($this->url_i2p) && empty($this->url_pkdns)) {
|
||||
$this->addError('url_clearnet', __('Mindestens eine URL muss angegeben werden.'));
|
||||
throw new \Illuminate\Validation\ValidationException(
|
||||
validator([], [])
|
||||
);
|
||||
}
|
||||
$this->country = request()->route('country', config('app.domain_country'));
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$validated = $this->validate();
|
||||
|
||||
$this->validateAtLeastOneUrl();
|
||||
|
||||
/** @var SelfHostedService $service */
|
||||
$service = SelfHostedService::create([
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'intro' => $validated['intro'] ?? null,
|
||||
'url_clearnet' => $validated['url_clearnet'] ?? null,
|
||||
'url_onion' => $validated['url_onion'] ?? null,
|
||||
'url_i2p' => $validated['url_i2p'] ?? null,
|
||||
'url_pkdns' => $validated['url_pkdns'] ?? null,
|
||||
'contact' => $validated['contact'] ?? null,
|
||||
'created_by' => $this->anonymous ? null : auth()->id(),
|
||||
]);
|
||||
|
||||
if ($this->logo) {
|
||||
$service
|
||||
->addMedia($this->logo->getRealPath())
|
||||
->usingFileName($this->logo->getClientOriginalName())
|
||||
->toMediaCollection('logo');
|
||||
}
|
||||
$service = $this->form->store();
|
||||
|
||||
session()->flash('status', __('Service erfolgreich erstellt!'));
|
||||
|
||||
redirect()->route('services.index', ['country' => request()->route('country')]);
|
||||
redirect()->route('services.index', ['country' => $this->country]);
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'types' => collect(SelfHostedServiceType::cases())->map(fn($c) => [
|
||||
'value' => $c->value, 'label' => ucfirst($c->value)
|
||||
'value' => $c->value, 'label' => $c->label()
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -105,57 +50,37 @@ class extends Component {
|
||||
<flux:legend>{{ __('Grundlegende Informationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<flux:file-upload wire:model="logo">
|
||||
<div class="
|
||||
relative flex items-center justify-center size-20 rounded transition-colors cursor-pointer
|
||||
border border-zinc-200 dark:border-white/10 hover:border-zinc-300 dark:hover:border-white/10
|
||||
bg-zinc-100 hover:bg-zinc-200 dark:bg-white/10 hover:dark:bg-white/15 in-data-dragging:dark:bg-white/15
|
||||
">
|
||||
@if($logo)
|
||||
<img src="{{ $logo?->temporaryUrl() }}" alt="Logo"
|
||||
class="size-full object-cover rounded"/>
|
||||
@else
|
||||
<flux:icon name="cube" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
@endif
|
||||
|
||||
<div class="absolute bottom-0 right-0 bg-white dark:bg-zinc-800 rounded">
|
||||
<flux:icon name="arrow-up-circle" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
</div>
|
||||
</div>
|
||||
</flux:file-upload>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="name" placeholder="Name" required/>
|
||||
<flux:input wire:model="form.name" placeholder="Name" required/>
|
||||
<flux:description>{{ __('Der Name des Services') }}</flux:description>
|
||||
<flux:error name="name"/>
|
||||
<flux:error name="form.name"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Typ') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="type" placeholder="{{ __('Bitte wählen') }}" required>
|
||||
<flux:select wire:model="form.type" placeholder="{{ __('Bitte wählen') }}" required>
|
||||
<flux:select.option :value="null">—</flux:select.option>
|
||||
@foreach($types as $t)
|
||||
<flux:select.option value="{{ $t['value'] }}">{{ $t['label'] }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Art des Services') }}</flux:description>
|
||||
<flux:error name="type"/>
|
||||
<flux:error name="form.type"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anonym einstellen') }}</flux:label>
|
||||
<flux:switch wire:model="anonymous"/>
|
||||
<flux:switch wire:model="form.anonymous"/>
|
||||
<flux:description>{{ __('Service ohne Autorenangabe einstellen') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||
<flux:textarea rows="4" wire:model="intro"/>
|
||||
<flux:textarea rows="4" wire:model="form.intro"/>
|
||||
<flux:description>{{ __('Kurze Beschreibung des Services') }}</flux:description>
|
||||
<flux:error name="intro"/>
|
||||
<flux:error name="form.intro"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
@@ -166,39 +91,38 @@ class extends Component {
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (Clearnet)') }}</flux:label>
|
||||
<flux:input wire:model="url_clearnet" type="url" placeholder="https://..."/>
|
||||
<flux:input wire:model="form.url_clearnet" type="url" placeholder="https://..."/>
|
||||
<flux:description>{{ __('Normale Web-URL') }}</flux:description>
|
||||
<flux:error name="url_clearnet"/>
|
||||
<flux:error name="form.url_clearnet"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (Onion/Tor)') }}</flux:label>
|
||||
<flux:input wire:model="url_onion" placeholder="http://...onion"/>
|
||||
<flux:input wire:model="form.url_onion" placeholder="http://...onion"/>
|
||||
<flux:description>{{ __('Tor Hidden Service URL') }}</flux:description>
|
||||
<flux:error name="url_onion"/>
|
||||
<flux:error name="form.url_onion"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (I2P)') }}</flux:label>
|
||||
<flux:input wire:model="url_i2p" placeholder="..."/>
|
||||
<flux:input wire:model="form.url_i2p" placeholder="..."/>
|
||||
<flux:description>{{ __('I2P Adresse') }}</flux:description>
|
||||
<flux:error name="url_i2p"/>
|
||||
<flux:error name="form.url_i2p"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (pkdns)') }}</flux:label>
|
||||
<flux:input wire:model="url_pkdns" placeholder="..."/>
|
||||
<flux:input wire:model="form.url_pkdns" placeholder="..."/>
|
||||
<flux:description>{{ __('Pkarr DNS Adresse') }}</flux:description>
|
||||
<flux:error name="url_pkdns"/>
|
||||
<flux:error name="form.url_pkdns"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kontaktinformation') }}</flux:label>
|
||||
<flux:textarea rows="3" wire:model="contact"
|
||||
placeholder="{{ __('Signal: username, SimpleX: https://..., Email: ...') }}"/>
|
||||
<flux:textarea rows="3" wire:model="form.contact" placeholder="{{ __('Signal: @username, SimpleX: https://..., Email: ...') }}"/>
|
||||
<flux:description>{{ __('Beliebige Kontaktinformationen (Signal, SimpleX, Email, etc.)') }}</flux:description>
|
||||
<flux:error name="contact"/>
|
||||
<flux:error name="form.contact"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Attributes\SeoDataAttribute;
|
||||
use App\Enums\SelfHostedServiceType;
|
||||
use App\Livewire\Forms\ServiceForm;
|
||||
use App\Models\SelfHostedService;
|
||||
use App\Traits\SeoTrait;
|
||||
use Livewire\Attributes\Validate;
|
||||
@@ -11,39 +12,17 @@ use Livewire\WithFileUploads;
|
||||
new
|
||||
#[SeoDataAttribute(key: 'services_edit')]
|
||||
class extends Component {
|
||||
use WithFileUploads;
|
||||
use SeoTrait;
|
||||
|
||||
public SelfHostedService $service;
|
||||
|
||||
#[Validate('image|max:10240')] // 10MB
|
||||
public $logo;
|
||||
|
||||
public string $name = '';
|
||||
public ?string $intro = null;
|
||||
public ?string $url_clearnet = null;
|
||||
public ?string $url_onion = null;
|
||||
public ?string $url_i2p = null;
|
||||
public ?string $url_pkdns = null;
|
||||
public ?string $type = null;
|
||||
public ?string $contact = null;
|
||||
public bool $anonymous = false;
|
||||
public ServiceForm $form;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$this->service->load('media');
|
||||
|
||||
$this->name = $this->service->name;
|
||||
$this->intro = $this->service->intro;
|
||||
$this->url_clearnet = $this->service->url_clearnet;
|
||||
$this->url_onion = $this->service->url_onion;
|
||||
$this->url_i2p = $this->service->url_i2p;
|
||||
$this->url_pkdns = $this->service->url_pkdns;
|
||||
$this->type = $this->service->type?->value ?? null;
|
||||
$this->contact = $this->service->contact;
|
||||
$this->anonymous = is_null($this->service->created_by);
|
||||
$this->form->setService($this->service);
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
@@ -54,59 +33,11 @@ class extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'in:'.collect(SelfHostedServiceType::cases())->map(fn($c) => $c->value)->implode(',')],
|
||||
'intro' => ['nullable', 'string'],
|
||||
'url_clearnet' => ['nullable', 'url', 'max:255'],
|
||||
'url_onion' => ['nullable', 'string', 'max:255'],
|
||||
'url_i2p' => ['nullable', 'string', 'max:255'],
|
||||
'url_pkdns' => ['nullable', 'string', 'max:255'],
|
||||
'contact' => ['nullable', 'string'],
|
||||
'anonymous' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function validateAtLeastOneUrl(): void
|
||||
{
|
||||
if (empty($this->url_clearnet) && empty($this->url_onion) && empty($this->url_i2p) && empty($this->url_pkdns)) {
|
||||
$this->addError('url_clearnet', __('Mindestens eine URL muss angegeben werden.'));
|
||||
throw new \Illuminate\Validation\ValidationException(
|
||||
validator([], [])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$validated = $this->validate();
|
||||
|
||||
$this->validateAtLeastOneUrl();
|
||||
|
||||
$this->service->update([
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'intro' => $validated['intro'] ?? null,
|
||||
'url_clearnet' => $validated['url_clearnet'] ?? null,
|
||||
'url_onion' => $validated['url_onion'] ?? null,
|
||||
'url_i2p' => $validated['url_i2p'] ?? null,
|
||||
'url_pkdns' => $validated['url_pkdns'] ?? null,
|
||||
'contact' => $validated['contact'] ?? null,
|
||||
'created_by' => $this->anonymous ? null : ($this->service->created_by ?? auth()->id()),
|
||||
]);
|
||||
|
||||
if ($this->logo) {
|
||||
$this->service->clearMediaCollection('logo');
|
||||
$this->service->addMedia($this->logo->getRealPath())
|
||||
->usingFileName($this->logo->getClientOriginalName())
|
||||
->toMediaCollection('logo');
|
||||
$this->logo = null;
|
||||
$this->service->load('media');
|
||||
}
|
||||
$this->form->update();
|
||||
|
||||
session()->flash('status', __('Service erfolgreich aktualisiert!'));
|
||||
}
|
||||
@@ -114,7 +45,9 @@ class extends Component {
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'types' => collect(SelfHostedServiceType::cases())->map(fn($c) => ['value' => $c->value, 'label' => ucfirst($c->value)]),
|
||||
'types' => collect(SelfHostedServiceType::cases())->map(fn($c) => [
|
||||
'value' => $c->value, 'label' => $c->label()
|
||||
]),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
@@ -129,29 +62,6 @@ class extends Component {
|
||||
<flux:legend>{{ __('Grundlegende Informationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<flux:file-upload wire:model="logo">
|
||||
<div class="
|
||||
relative flex items-center justify-center size-20 rounded transition-colors cursor-pointer
|
||||
border border-zinc-200 dark:border-white/10 hover:border-zinc-300 dark:hover:border-white/10
|
||||
bg-zinc-100 hover:bg-zinc-200 dark:bg-white/10 hover:dark:bg-white/15 in-data-dragging:dark:bg-white/15
|
||||
">
|
||||
@if (!$logo && $service->getFirstMedia('logo'))
|
||||
<img src="{{ $service->getFirstMediaUrl('logo') }}" alt="Logo"
|
||||
class="size-full object-cover rounded"/>
|
||||
@elseif($logo)
|
||||
<img src="{{ $logo?->temporaryUrl() }}" alt="Logo"
|
||||
class="size-full object-cover rounded"/>
|
||||
@else
|
||||
<flux:icon name="cube" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
@endif
|
||||
|
||||
<div class="absolute bottom-0 right-0 bg-white dark:bg-zinc-800 rounded">
|
||||
<flux:icon name="arrow-up-circle" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
</div>
|
||||
</div>
|
||||
</flux:file-upload>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('ID') }}</flux:label>
|
||||
<flux:input value="{{ $service->id }}" disabled/>
|
||||
@@ -160,35 +70,35 @@ class extends Component {
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="name" required/>
|
||||
<flux:input wire:model="form.name" required/>
|
||||
<flux:description>{{ __('Der Name des Services') }}</flux:description>
|
||||
<flux:error name="name"/>
|
||||
<flux:error name="form.name"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Typ') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="type" placeholder="{{ __('Bitte wählen') }}" required>
|
||||
<flux:select wire:model="form.type" placeholder="{{ __('Bitte wählen') }}" required>
|
||||
<flux:select.option :value="null">—</flux:select.option>
|
||||
@foreach($types as $t)
|
||||
<flux:select.option value="{{ $t['value'] }}">{{ $t['label'] }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Art des Services') }}</flux:description>
|
||||
<flux:error name="type"/>
|
||||
<flux:error name="form.type"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anonym') }}</flux:label>
|
||||
<flux:switch wire:model="anonymous"/>
|
||||
<flux:switch wire:model="form.anonymous"/>
|
||||
<flux:description>{{ __('Service ohne Autorenangabe') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||
<flux:textarea rows="4" wire:model="intro"/>
|
||||
<flux:textarea rows="4" wire:model="form.intro"/>
|
||||
<flux:description>{{ __('Kurze Beschreibung des Services') }}</flux:description>
|
||||
<flux:error name="intro"/>
|
||||
<flux:error name="form.intro"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
@@ -199,38 +109,38 @@ class extends Component {
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (Clearnet)') }}</flux:label>
|
||||
<flux:input wire:model="url_clearnet" type="url" placeholder="https://..."/>
|
||||
<flux:input wire:model="form.url_clearnet" type="url" placeholder="https://..."/>
|
||||
<flux:description>{{ __('Normale Web-URL') }}</flux:description>
|
||||
<flux:error name="url_clearnet"/>
|
||||
<flux:error name="form.url_clearnet"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (Onion/Tor)') }}</flux:label>
|
||||
<flux:input wire:model="url_onion" placeholder="http://...onion"/>
|
||||
<flux:input wire:model="form.url_onion" placeholder="http://...onion"/>
|
||||
<flux:description>{{ __('Tor Hidden Service URL') }}</flux:description>
|
||||
<flux:error name="url_onion"/>
|
||||
<flux:error name="form.url_onion"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (I2P)') }}</flux:label>
|
||||
<flux:input wire:model="url_i2p" placeholder="..."/>
|
||||
<flux:input wire:model="form.url_i2p" placeholder="..."/>
|
||||
<flux:description>{{ __('I2P Adresse') }}</flux:description>
|
||||
<flux:error name="url_i2p"/>
|
||||
<flux:error name="form.url_i2p"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL (pkdns)') }}</flux:label>
|
||||
<flux:input wire:model="url_pkdns" placeholder="..."/>
|
||||
<flux:input wire:model="form.url_pkdns" placeholder="..."/>
|
||||
<flux:description>{{ __('Pkarr DNS Adresse') }}</flux:description>
|
||||
<flux:error name="url_pkdns"/>
|
||||
<flux:error name="form.url_pkdns"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kontaktinformation') }}</flux:label>
|
||||
<flux:textarea rows="3" wire:model="contact" placeholder="{{ __('Signal: @username, SimpleX: https://..., Email: ...') }}"/>
|
||||
<flux:textarea rows="3" wire:model="form.contact" placeholder="{{ __('Signal: @username, SimpleX: https://..., Email: ...') }}"/>
|
||||
<flux:description>{{ __('Beliebige Kontaktinformationen (Signal, SimpleX, Email, etc.)') }}</flux:description>
|
||||
<flux:error name="contact"/>
|
||||
<flux:error name="form.contact"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
|
||||
@@ -14,19 +14,29 @@ class extends Component {
|
||||
|
||||
public string $country = 'de';
|
||||
public string $search = '';
|
||||
public ?string $typeFilter = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->country = request()->route('country', config('app.domain_country'));
|
||||
}
|
||||
|
||||
public function filterByType(?string $type): void
|
||||
{
|
||||
$this->typeFilter = $this->typeFilter === $type ? null : $type;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'services' => SelfHostedService::query()
|
||||
->with('createdBy')
|
||||
->when($this->search, fn($q) => $q->where('name', 'ilike', '%'.$this->search.'%'))
|
||||
->when($this->typeFilter, fn($q) => $q->where('type', $this->typeFilter))
|
||||
->orderBy('name')
|
||||
->paginate(15),
|
||||
'types' => \App\Enums\SelfHostedServiceType::cases(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
@@ -44,68 +54,108 @@ class extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type Filter Cloud -->
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
@foreach($types as $type)
|
||||
<flux:badge
|
||||
wire:click="filterByType('{{ $type->value }}')"
|
||||
size="lg"
|
||||
color="{{ $type->color() }}"
|
||||
class="cursor-pointer transition-opacity {{ $typeFilter === $type->value ? 'ring-2 ring-offset-2' : 'opacity-70 hover:opacity-100' }}"
|
||||
>
|
||||
{{ $type->label() }}
|
||||
</flux:badge>
|
||||
@endforeach
|
||||
@if($typeFilter)
|
||||
<flux:badge
|
||||
wire:click="filterByType(null)"
|
||||
size="lg"
|
||||
color="zinc"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<flux:icon.x-mark variant="mini" class="inline" />
|
||||
{{ __('Filter zurücksetzen') }}
|
||||
</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<flux:table :paginate="$services" class="mt-6">
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Name') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Typ') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Links') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Erstellt von') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Datum') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($services as $service)
|
||||
<flux:table.row :key="$service->id">
|
||||
<flux:table.cell variant="strong" class="flex items-center gap-3">
|
||||
<flux:avatar class="[:where(&)]:size-16" :href="route('services.landingpage', ['service' => $service, 'country' => $country])" src="{{ $service->getFirstMedia('logo') ? $service->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<flux:table.cell variant="strong">
|
||||
<a href="{{ route('services.landingpage', ['service' => $service, 'country' => $country]) }}">{{ $service->name }}</a>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@if($service->type)
|
||||
<flux:badge size="sm">{{ ucfirst($service->type->value) }}</flux:badge>
|
||||
<flux:badge size="sm" color="{{ $service->type->color() }}">
|
||||
{{ $service->type->label() }}
|
||||
</flux:badge>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($service->url_clearnet)
|
||||
<flux:link :href="$service->url_clearnet" external variant="subtle" title="Clearnet">
|
||||
<flux:icon.globe-alt variant="mini" />
|
||||
<flux:link :href="$service->url_clearnet" external class="text-blue-600 dark:text-blue-400">
|
||||
<flux:icon.globe-alt variant="mini" class="inline" />
|
||||
Clearnet
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->url_onion)
|
||||
<flux:link :href="$service->url_onion" external variant="subtle" title="Onion">
|
||||
<flux:icon.lock-closed variant="mini" />
|
||||
<flux:link :href="$service->url_onion" external class="text-purple-600 dark:text-purple-400">
|
||||
<flux:icon.lock-closed variant="mini" class="inline" />
|
||||
Onion
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->url_i2p)
|
||||
<flux:link :href="$service->url_i2p" external variant="subtle" title="I2P">
|
||||
<flux:icon.link variant="mini" />
|
||||
<flux:link :href="$service->url_i2p" external class="text-green-600 dark:text-green-400">
|
||||
<flux:icon.link variant="mini" class="inline" />
|
||||
I2P
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->url_pkdns)
|
||||
<flux:link :href="$service->url_pkdns" external variant="subtle" title="pkdns">
|
||||
<flux:icon.link variant="mini" />
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->contact_url)
|
||||
<flux:link :href="$service->contact_url" external variant="subtle" title="Kontakt">
|
||||
<flux:icon.envelope variant="mini" />
|
||||
<flux:link :href="$service->url_pkdns" external class="text-orange-600 dark:text-orange-400">
|
||||
<flux:icon.link variant="mini" class="inline" />
|
||||
pkdns
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@auth
|
||||
@if(auth()->id() === $service->created_by)
|
||||
<flux:button :href="route_with_country('services.edit', ['service' => $service])" size="xs" variant="filled" icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($service->createdBy)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:avatar size="xs" src="{{ $service->createdBy->profile_photo_url }}" />
|
||||
<span>{{ Str::length($service->createdBy->name) > 10 ? Str::substr($service->createdBy->name, 0, 4) . '...' . Str::substr($service->createdBy->name, -3) : $service->createdBy->name }}</span>
|
||||
</div>
|
||||
@else
|
||||
<flux:link :href="route('login')">{{ __('Log in') }}</flux:link>
|
||||
@endauth
|
||||
<span class="text-gray-500 dark:text-gray-400 italic">{{ __('Anonymous') }}</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.plus variant="micro" class="text-green-600 dark:text-green-400" />
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ $service->created_at->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
@if($service->created_at->ne($service->updated_at))
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.pencil variant="micro" class="text-blue-600 dark:text-blue-400" />
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ $service->updated_at->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
|
||||
@@ -16,6 +16,7 @@ class extends Component {
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->service->load('createdBy');
|
||||
$this->country = request()->route('country', config('app.domain_country'));
|
||||
}
|
||||
|
||||
@@ -27,63 +28,11 @@ class extends Component {
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:avatar class="[:where(&)]:size-24 [:where(&)]:text-base" size="xl"
|
||||
src="{{ $service->getFirstMediaUrl('logo') }}"/>
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-1">{{ $service->name }}</flux:heading>
|
||||
@if($service->type)
|
||||
<flux:badge size="sm">{{ ucfirst($service->type->value) }}</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($service->intro)
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Über den Service') }}</flux:heading>
|
||||
<x-markdown class="prose whitespace-pre-wrap">{!! $service->intro !!}</x-markdown>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:heading size="lg">{{ __('Links') }}</flux:heading>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
@if($service->url_clearnet)
|
||||
<flux:button href="{{ $service->url_clearnet }}" target="_blank" variant="ghost" class="justify-start">
|
||||
<flux:icon.globe-alt class="w-5 h-5 mr-2" />
|
||||
Clearnet
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($service->url_onion)
|
||||
<flux:button href="{{ $service->url_onion }}" target="_blank" variant="ghost" class="justify-start">
|
||||
<flux:icon.lock-closed class="w-5 h-5 mr-2" />
|
||||
Onion / Tor
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($service->url_i2p)
|
||||
<flux:button href="{{ $service->url_i2p }}" target="_blank" variant="ghost" class="justify-start">
|
||||
<flux:icon.link class="w-5 h-5 mr-2" />
|
||||
I2P
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($service->url_pkdns)
|
||||
<flux:button href="{{ $service->url_pkdns }}" target="_blank" variant="ghost" class="justify-start">
|
||||
<flux:icon.link class="w-5 h-5 mr-2" />
|
||||
pkdns
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($service->contact_url)
|
||||
<flux:button href="{{ $service->contact_url }}" target="_blank" variant="ghost" class="justify-start">
|
||||
<flux:icon.envelope class="w-5 h-5 mr-2" />
|
||||
{{ __('Kontakt') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 py-8 max-w-5xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<flux:heading size="xl">{{ $service->name }}</flux:heading>
|
||||
@auth
|
||||
@if(auth()->id() === $service->created_by)
|
||||
<flux:button :href="route_with_country('services.edit', ['service' => $service])" variant="primary" icon="pencil">
|
||||
@@ -92,5 +41,114 @@ class extends Component {
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@if($service->type)
|
||||
<flux:badge size="lg" color="{{ $service->type->color() }}">
|
||||
{{ $service->type->label() }}
|
||||
</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- Description -->
|
||||
@if($service->intro)
|
||||
<flux:card class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Beschreibung') }}</flux:heading>
|
||||
<div class="prose dark:prose-invert max-w-none whitespace-pre-wrap">
|
||||
{{ $service->intro }}
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
<!-- Contact Information -->
|
||||
@if($service->contact)
|
||||
<flux:card class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Kontakt') }}</flux:heading>
|
||||
<div class="prose dark:prose-invert max-w-none whitespace-pre-wrap">
|
||||
{{ $service->contact }}
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Links -->
|
||||
<flux:card class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Zugriff') }}</flux:heading>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if($service->url_clearnet)
|
||||
<flux:link :href="$service->url_clearnet" external class="text-blue-600 dark:text-blue-400 flex items-center gap-2">
|
||||
<flux:icon.globe-alt variant="mini" />
|
||||
<span>Clearnet</span>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->url_onion)
|
||||
<flux:link :href="$service->url_onion" external class="text-purple-600 dark:text-purple-400 flex items-center gap-2">
|
||||
<flux:icon.lock-closed variant="mini" />
|
||||
<span>Onion / Tor</span>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->url_i2p)
|
||||
<flux:link :href="$service->url_i2p" external class="text-green-600 dark:text-green-400 flex items-center gap-2">
|
||||
<flux:icon.link variant="mini" />
|
||||
<span>I2P</span>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($service->url_pkdns)
|
||||
<flux:link :href="$service->url_pkdns" external class="text-orange-600 dark:text-orange-400 flex items-center gap-2">
|
||||
<flux:icon.link variant="mini" />
|
||||
<span>pkdns</span>
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<!-- Metadata -->
|
||||
<flux:card class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Informationen') }}</flux:heading>
|
||||
<div class="space-y-4 text-sm">
|
||||
<!-- Created By -->
|
||||
<div>
|
||||
<div class="text-gray-500 dark:text-gray-400 mb-1">{{ __('Erstellt von') }}</div>
|
||||
@if($service->createdBy)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:avatar size="xs" src="{{ $service->createdBy->profile_photo_url }}" />
|
||||
<span class="font-medium">{{ $service->createdBy->name }}</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-500 dark:text-gray-400 italic">{{ __('Anonymous') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Created At -->
|
||||
<div>
|
||||
<div class="text-gray-500 dark:text-gray-400 mb-1">{{ __('Erstellt am') }}</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.plus variant="micro" class="text-green-600 dark:text-green-400" />
|
||||
<span>{{ $service->created_at->format('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Updated At -->
|
||||
@if($service->created_at->ne($service->updated_at))
|
||||
<div>
|
||||
<div class="text-gray-500 dark:text-gray-400 mb-1">{{ __('Zuletzt aktualisiert') }}</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.pencil variant="micro" class="text-blue-600 dark:text-blue-400" />
|
||||
<span>{{ $service->updated_at->format('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<!-- Back Button -->
|
||||
<flux:button :href="route_with_country('services.index')" variant="ghost" icon="arrow-left" class="w-full">
|
||||
{{ __('Zurück zur Übersicht') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,11 @@ it('creates a self hosted service', function () {
|
||||
->set('name', 'My Node')
|
||||
->set('type', SelfHostedServiceType::Mempool->value)
|
||||
->set('url_clearnet', 'https://example.com')
|
||||
->set('contact_url', 'https://contact.example.com')
|
||||
->set('contact', ['url' => 'https://contact.example.com'])
|
||||
->call('save');
|
||||
|
||||
expect(SelfHostedService::where('name', 'My Node')->exists())->toBeTrue();
|
||||
|
||||
$service = SelfHostedService::where('name', 'My Node')->first();
|
||||
expect($service->getFirstMedia('logo'))->toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user