mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-02-15 03:23:17 +00:00
Die News Ansicht verschönern (vibe-kanban 7c9cbf57)
Nutze Livewire Flux UI um die Ansicht der News Items zu verschönern. Teste alles am besten auch eine Validierung mit playwright, ob alles passt und richtig in mobile und Desktop angezeigt wird.
This commit is contained in:
@@ -142,291 +142,233 @@ class extends Component {
|
||||
|
||||
<div>
|
||||
@if($isAllowed)
|
||||
<div class="xl:flex">
|
||||
<div class="lg:flex gap-8">
|
||||
|
||||
<!-- Left + Middle content -->
|
||||
<div class="md:flex flex-1">
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<flux:heading size="xl">News</flux:heading>
|
||||
</div>
|
||||
|
||||
<!-- Left content -->
|
||||
<div class="w-full md:w-60 mb-8 md:mb-0">
|
||||
<div
|
||||
class="md:sticky md:top-16 md:h-[calc(100dvh-64px)] md:overflow-x-hidden md:overflow-y-auto no-scrollbar">
|
||||
<div class="md:py-8">
|
||||
<!-- Category filter (horizontal, scrollable on mobile) -->
|
||||
<div class="mb-6 flex flex-nowrap gap-2 overflow-x-auto no-scrollbar pb-1">
|
||||
<flux:badge
|
||||
as="button"
|
||||
wire:click="clearFilter"
|
||||
:color="$selectedCategory === null ? 'amber' : 'zinc'"
|
||||
:variant="$selectedCategory === null ? 'solid' : 'outline'"
|
||||
size="sm"
|
||||
class="shrink-0 cursor-pointer"
|
||||
>
|
||||
Alle
|
||||
</flux:badge>
|
||||
@foreach(\App\Enums\NewsCategory::selectOptions() as $category)
|
||||
<flux:badge
|
||||
wire:key="cat_{{ $category['value'] }}"
|
||||
as="button"
|
||||
wire:click="filterByCategory({{ $category['value'] }})"
|
||||
:color="$selectedCategory === $category['value'] ? 'amber' : 'zinc'"
|
||||
:variant="$selectedCategory === $category['value'] ? 'solid' : 'outline'"
|
||||
size="sm"
|
||||
class="shrink-0 cursor-pointer"
|
||||
>
|
||||
<i class="fa-sharp-duotone fa-solid fa-{{ $category['icon'] }} mr-1"></i>
|
||||
{{ $category['label'] }}
|
||||
</flux:badge>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center md:block">
|
||||
|
||||
<!-- Title -->
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl md:text-3xl text-zinc-800 dark:text-zinc-100 font-bold">
|
||||
News
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div
|
||||
class="flex flex-nowrap overflow-x-scroll no-scrollbar md:block md:overflow-auto px-4 md:space-y-3 -mx-4">
|
||||
<!-- Group 1 -->
|
||||
<div>
|
||||
<div
|
||||
class="text-xs font-semibold text-zinc-400 dark:text-zinc-500 uppercase mb-3 md:sr-only">
|
||||
Kategorien
|
||||
</div>
|
||||
<ul class="flex flex-nowrap md:block mr-3 md:mr-0">
|
||||
<li class="mr-0.5 md:mr-0 md:mb-0.5" wire:key="category_all">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="clearFilter"
|
||||
@class([
|
||||
'inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium transition-colors cursor-pointer',
|
||||
'bg-amber-500 text-white' => $selectedCategory === null,
|
||||
'bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-600' => $selectedCategory !== null,
|
||||
])
|
||||
>
|
||||
<i class="fa-sharp-duotone fa-solid fa-layer-group shrink-0 fill-current mr-2"></i>
|
||||
<span>Alle</span>
|
||||
</button>
|
||||
</li>
|
||||
@foreach(\App\Enums\NewsCategory::selectOptions() as $category)
|
||||
<li class="mr-0.5 md:mr-0 md:mb-0.5"
|
||||
wire:key="category_{{ $category['value'] }}">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="filterByCategory({{ $category['value'] }})"
|
||||
@class([
|
||||
'inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium transition-colors cursor-pointer',
|
||||
'bg-amber-500 text-white' => $selectedCategory === $category['value'],
|
||||
'bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-600' => $selectedCategory !== $category['value'],
|
||||
])
|
||||
>
|
||||
<i class="fa-sharp-duotone fa-solid fa-{{ $category['icon'] }} shrink-0 fill-current mr-2"></i>
|
||||
<span>{{ $category['label'] }}</span>
|
||||
</button>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<!-- News list -->
|
||||
<div class="space-y-4">
|
||||
@forelse($this->filteredNews as $post)
|
||||
<flux:card wire:key="post_{{ $post->id }}" class="space-y-0">
|
||||
<!-- Header row: avatar + meta + actions -->
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:avatar
|
||||
size="sm"
|
||||
:src="$post->einundzwanzigPleb->profile?->picture ?? asset('einundzwanzig-alpha.jpg')"
|
||||
:name="$post->einundzwanzigPleb->profile?->name ?? 'Anonym'"
|
||||
circle
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-1">
|
||||
<flux:text class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{{ $post->einundzwanzigPleb?->profile?->name ?? str($post->einundzwanzigPleb?->npub)->limit(32) }}
|
||||
</flux:text>
|
||||
<flux:text class="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
{{ $post->created_at->format('d.m.Y') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<flux:badge
|
||||
as="button"
|
||||
wire:click="filterByCategory({{ $post->category->value }})"
|
||||
:color="$post->category->color()"
|
||||
size="sm"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<i class="fa-sharp-duotone fa-solid fa-{{ $post->category->icon() }} mr-1"></i>
|
||||
{{ $post->category->label() }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle content -->
|
||||
<div class="flex-1 md:ml-8 xl:mx-4 2xl:mx-8">
|
||||
<div class="md:py-8">
|
||||
|
||||
<div class="space-y-2">
|
||||
@forelse($this->filteredNews as $post)
|
||||
<flux:card wire:key="post_{{ $post->id }}">
|
||||
<!-- Avatar -->
|
||||
<div class="shrink-0 mt-1.5">
|
||||
<img class="w-8 h-8 rounded-full"
|
||||
src="{{ $post->einundzwanzigPleb->profile?->picture ?? asset('einundzwanzig-alpha.jpg') }}"
|
||||
width="32" height="32"
|
||||
alt="{{ $post->einundzwanzigPleb->profile?->name }}">
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="grow">
|
||||
<!-- Category Badge -->
|
||||
<div class="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="filterByCategory({{ $post->category->value }})"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<i class="fa-sharp-duotone fa-solid fa-{{ $post->category->icon() }} mr-1"></i>
|
||||
{{ $post->category->label() }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Title -->
|
||||
<h2 class="font-semibold text-zinc-800 dark:text-zinc-100 mb-2">
|
||||
{{ $post->name }}
|
||||
</h2>
|
||||
<p class="mb-6">
|
||||
{{ $post->description }}
|
||||
</p>
|
||||
<!-- Footer -->
|
||||
<footer class="flex flex-wrap text-sm">
|
||||
<div
|
||||
class="flex items-center after:block after:content-['·'] last:after:content-[''] after:text-sm after:text-zinc-400 dark:after:text-zinc-600 after:px-2">
|
||||
<div
|
||||
class="font-medium text-amber-500 hover:text-amber-600 dark:hover:text-amber-400">
|
||||
<div class="flex items-center">
|
||||
<svg class="mr-2 fill-current" width="16"
|
||||
height="16"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.686 5.708 10.291.313c-.4-.4-.999-.4-1.399 0s-.4 1 0 1.399l.6.6-6.794 3.696-1-1C1.299 4.61.7 4.61.3 5.009c-.4.4-.4 1 0 1.4l1.498 1.498 2.398 2.398L.6 14.001 2 15.4l3.696-3.697L9.692 15.7c.5.5 1.199.2 1.398 0 .4-.4.4-1 0-1.4l-.999-.998 3.697-6.695.6.6c.599.6 1.199.2 1.398 0 .3-.4.3-1.1-.1-1.499Zm-7.193 6.095L4.196 7.507l6.695-3.697 1.298 1.299-3.696 6.694Z"></path>
|
||||
</svg>
|
||||
{{ $post->einundzwanzigPleb?->profile?->name ?? str($post->einundzwanzigPleb?->npub)->limit(32) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center after:block after:content-['·'] last:after:content-[''] after:text-sm after:text-zinc-400 dark:after:text-zinc-600 after:px-2">
|
||||
<span
|
||||
class="text-zinc-500">{{ $post->created_at->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-end w-full space-x-2">
|
||||
@if($post->getFirstMedia('pdf'))
|
||||
<flux:button
|
||||
xs
|
||||
target="_blank"
|
||||
:href="url()->temporarySignedRoute('media.signed', now()->addMinutes(30), ['media' => $post->getFirstMedia('pdf')])"
|
||||
icon="cloud-arrow-down">
|
||||
Öffnen
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($canEdit)
|
||||
<flux:modal.trigger name="delete-news-{{ $post->id }}">
|
||||
<flux:button
|
||||
xs
|
||||
variant="danger"
|
||||
icon="trash"
|
||||
wire:click="confirmDelete({{ $post->id }})">
|
||||
Löschen
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:modal name="delete-news-{{ $post->id }}" class="min-w-88">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">News löschen?</flux:heading>
|
||||
<flux:text class="mt-2">
|
||||
Du bist dabei, diese News zu löschen.<br>
|
||||
Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</flux:text>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:spacer />
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">Abbrechen</flux:button>
|
||||
</flux:modal.close>
|
||||
<flux:button wire:click="delete" variant="danger">Löschen</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
@empty
|
||||
<flux:card>
|
||||
@if($selectedCategory !== null)
|
||||
<p>Keine News in dieser Kategorie vorhanden.</p>
|
||||
<flux:button wire:click="clearFilter" size="sm" class="mt-2">
|
||||
Alle anzeigen
|
||||
</flux:button>
|
||||
@else
|
||||
<p>Keine News vorhanden.</p>
|
||||
@endif
|
||||
</flux:card>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right content -->
|
||||
<div class="w-full mt-8 sm:mt-0 xl:w-72">
|
||||
<div
|
||||
class="lg:sticky lg:top-16 lg:h-[calc(100dvh-64px)] lg:overflow-x-hidden lg:overflow-y-auto no-scrollbar">
|
||||
<div class="md:py-8">
|
||||
|
||||
<!-- Blocks -->
|
||||
<div class="space-y-4">
|
||||
|
||||
@if($canEdit)
|
||||
<flux:card>
|
||||
<div
|
||||
class="text-xs font-semibold text-zinc-400 dark:text-zinc-200 uppercase mb-4">
|
||||
News anlegen
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col space-y-2">
|
||||
<flux:file-upload wire:model="file" label="PDF hochladen">
|
||||
<flux:file-upload.dropzone heading="Drop file here or click to browse" text="PDF bis 10MB" />
|
||||
</flux:file-upload>
|
||||
@error('file')
|
||||
<span class="text-red-500">{{ $message }}</span>
|
||||
@enderror
|
||||
<div class="mt-3 flex flex-col gap-2">
|
||||
@if ($file)
|
||||
<flux:file-item
|
||||
:heading="$file->getClientOriginalName()"
|
||||
:size="$file->getSize()"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<flux:file-item.remove wire:click="removeFile" aria-label="{{ 'Remove file: ' . $file->getClientOriginalName() }}" />
|
||||
</x-slot>
|
||||
</flux:file-item>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>Kategorie</flux:label>
|
||||
<flux:select
|
||||
wire:model="form.category"
|
||||
placeholder="Wähle Kategorie"
|
||||
>
|
||||
@foreach(\App\Enums\NewsCategory::selectOptions() as $category)
|
||||
<flux:select.option
|
||||
:label="$category['label']"
|
||||
:value="$category['value']"
|
||||
/>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.category"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>Titel</flux:label>
|
||||
<flux:input wire:model="form.name" placeholder="News-Titel"/>
|
||||
<flux:error name="form.name"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>Beschreibung</flux:label>
|
||||
<flux:description>optional</flux:description>
|
||||
<flux:textarea wire:model="form.description" rows="4"
|
||||
placeholder="Beschreibung..."/>
|
||||
<flux:error name="form.description"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
<flux:button wire:click="save" class="w-full">
|
||||
Hinzufügen
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
<!-- Body -->
|
||||
<div class="mt-3">
|
||||
<flux:heading class="mb-1">{{ $post->name }}</flux:heading>
|
||||
@if($post->description)
|
||||
<flux:text class="text-sm">{{ $post->description }}</flux:text>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
@if($post->getFirstMedia('pdf'))
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
target="_blank"
|
||||
:href="url()->temporarySignedRoute('media.signed', now()->addMinutes(30), ['media' => $post->getFirstMedia('pdf')])"
|
||||
icon="document-arrow-down"
|
||||
>
|
||||
PDF öffnen
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($canEdit)
|
||||
<flux:spacer />
|
||||
<flux:modal.trigger name="delete-news-{{ $post->id }}">
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
icon="trash"
|
||||
wire:click="confirmDelete({{ $post->id }})"
|
||||
>
|
||||
Löschen
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:modal name="delete-news-{{ $post->id }}" class="min-w-88">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">News löschen?</flux:heading>
|
||||
<flux:text class="mt-2">
|
||||
Du bist dabei, diese News zu löschen.<br>
|
||||
Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</flux:text>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:spacer />
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">Abbrechen</flux:button>
|
||||
</flux:modal.close>
|
||||
<flux:button wire:click="delete" variant="danger">Löschen</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
@empty
|
||||
<flux:card>
|
||||
<div class="py-6 text-center">
|
||||
@if($selectedCategory !== null)
|
||||
<flux:icon name="funnel" class="mx-auto mb-3 text-zinc-300 dark:text-zinc-600" />
|
||||
<flux:heading>Keine News in dieser Kategorie</flux:heading>
|
||||
<flux:text class="mt-1 text-sm">Versuche eine andere Kategorie oder zeige alle an.</flux:text>
|
||||
<flux:button wire:click="clearFilter" size="sm" class="mt-4">
|
||||
Alle anzeigen
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:icon name="newspaper" class="mx-auto mb-3 text-zinc-300 dark:text-zinc-600" />
|
||||
<flux:heading>Noch keine News vorhanden</flux:heading>
|
||||
<flux:text class="mt-1 text-sm">Hier werden zukünftige Neuigkeiten angezeigt.</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar: create form (board members only) -->
|
||||
@if($canEdit)
|
||||
<div class="w-full lg:w-80 shrink-0 mt-8 lg:mt-0">
|
||||
<div class="lg:sticky lg:top-16">
|
||||
<flux:card class="space-y-4">
|
||||
<flux:heading>News anlegen</flux:heading>
|
||||
<flux:separator />
|
||||
|
||||
<flux:file-upload wire:model="file" label="PDF hochladen">
|
||||
<flux:file-upload.dropzone heading="Datei hier ablegen oder klicken" text="PDF bis 10MB" />
|
||||
</flux:file-upload>
|
||||
<flux:error name="file" />
|
||||
|
||||
@if ($file)
|
||||
<flux:file-item
|
||||
:heading="$file->getClientOriginalName()"
|
||||
:size="$file->getSize()"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<flux:file-item.remove wire:click="removeFile" aria-label="{{ 'Remove file: ' . $file->getClientOriginalName() }}" />
|
||||
</x-slot>
|
||||
</flux:file-item>
|
||||
@endif
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Kategorie</flux:label>
|
||||
<flux:select
|
||||
wire:model="form.category"
|
||||
placeholder="Wähle Kategorie"
|
||||
>
|
||||
@foreach(\App\Enums\NewsCategory::selectOptions() as $category)
|
||||
<flux:select.option
|
||||
:label="$category['label']"
|
||||
:value="$category['value']"
|
||||
/>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.category" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Titel</flux:label>
|
||||
<flux:input wire:model="form.name" placeholder="News-Titel" />
|
||||
<flux:error name="form.name" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Beschreibung</flux:label>
|
||||
<flux:description>optional</flux:description>
|
||||
<flux:textarea wire:model="form.description" rows="4" placeholder="Beschreibung..." />
|
||||
<flux:error name="form.description" />
|
||||
</flux:field>
|
||||
|
||||
<flux:button wire:click="save" variant="primary" class="w-full">
|
||||
Hinzufügen
|
||||
</flux:button>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<flux:callout variant="warning" icon="exclamation-circle">
|
||||
<flux:heading>Zugriff auf News nicht möglich</flux:heading>
|
||||
<p>Um die News einzusehen, benötigst du:</p>
|
||||
<ul class="list-disc ml-5 mt-2 space-y-1">
|
||||
<li>Einen Vereinsstatus von "Aktives Mitglied"</li>
|
||||
<li>Eine bezahlte Mitgliedschaft für das aktuelle Jahr ({{ date('Y') }})</li>
|
||||
</ul>
|
||||
<p class="mt-3">
|
||||
@if(!NostrAuth::check())
|
||||
Bitte melde dich zunächst mit Nostr an.
|
||||
@else
|
||||
Bitte kontaktiere den Vorstand, wenn du denkst, dass du berechtigt sein solltest.
|
||||
@endif
|
||||
</p>
|
||||
<flux:callout.heading>Zugriff auf News nicht möglich</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
<p>Um die News einzusehen, benötigst du:</p>
|
||||
<ul class="list-disc ml-5 mt-2 space-y-1">
|
||||
<li>Einen Vereinsstatus von "Aktives Mitglied"</li>
|
||||
<li>Eine bezahlte Mitgliedschaft für das aktuelle Jahr ({{ date('Y') }})</li>
|
||||
</ul>
|
||||
<p class="mt-3">
|
||||
@if(!NostrAuth::check())
|
||||
Bitte melde dich zunächst mit Nostr an.
|
||||
@else
|
||||
Bitte kontaktiere den Vorstand, wenn du denkst, dass du berechtigt sein solltest.
|
||||
@endif
|
||||
</p>
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user