mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2025-12-13 05:26:47 +00:00
704 lines
39 KiB
PHP
704 lines
39 KiB
PHP
<?php
|
|
|
|
use Livewire\Volt\Component;
|
|
use swentel\nostr\{
|
|
Filter\Filter,
|
|
Key\Key,
|
|
Message\EventMessage,
|
|
Message\RequestMessage,
|
|
Relay\Relay,
|
|
Relay\RelaySet,
|
|
Request\Request,
|
|
Subscription\Subscription,
|
|
Event\Event as NostrEvent,
|
|
Sign\Sign
|
|
};
|
|
|
|
use function Livewire\Volt\{computed, mount, state, with, updated, on};
|
|
use function Laravel\Folio\{middleware, name};
|
|
|
|
name('association.election');
|
|
|
|
state([
|
|
'isAllowed' => false,
|
|
'showLog' => false,
|
|
'currentPubkey' => null,
|
|
'currentPleb' => null,
|
|
'events' => [],
|
|
'boardEvents' => [],
|
|
'election' => fn() => $election,
|
|
'plebs' => [],
|
|
'search' => '',
|
|
'signThisEvent' => '',
|
|
'isNotClosed' => true,
|
|
]);
|
|
|
|
mount(function () {
|
|
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
|
->with(['profile'])
|
|
->whereIn('association_status', [3, 4])
|
|
->orderBy('association_status', 'desc')
|
|
->get()
|
|
->toArray();
|
|
$this->loadEvents();
|
|
$this->loadBoardEvents();
|
|
if ($this->election->end_time?->isPast() || !config('services.voting')) {
|
|
$this->isNotClosed = false;
|
|
}
|
|
});
|
|
|
|
on([
|
|
'nostrLoggedIn' => function ($pubkey) {
|
|
$this->currentPubkey = $pubkey;
|
|
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
|
if ($this->currentPleb->association_status->value < 3) {
|
|
return $this->js('alert("Du bist nicht berechtigt, an der Wahl teilzunehmen.")');
|
|
}
|
|
$logPubkeys = [
|
|
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
|
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
|
];
|
|
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
|
$this->showLog = true;
|
|
}
|
|
$this->isAllowed = true;
|
|
},
|
|
'echo:votes,.newVote' => function () {
|
|
$this->loadEvents();
|
|
$this->loadBoardEvents();
|
|
},
|
|
'nostrLoggedOut' => function () {
|
|
$this->isAllowed = false;
|
|
$this->currentPubkey = null;
|
|
$this->currentPleb = null;
|
|
},
|
|
]);
|
|
|
|
updated([
|
|
'search' => function ($value) {
|
|
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
|
->with(['profile'])
|
|
->whereIn('association_status', [3, 4])
|
|
->where(fn($query)
|
|
=> $query
|
|
->where('pubkey', 'like', "%$value%")
|
|
->orWhereHas('profile', fn($query) => $query->where('name', 'ilike', "%$value%")))
|
|
->orderBy('association_status', 'desc')
|
|
->get()
|
|
->toArray();
|
|
},
|
|
]);
|
|
|
|
$loadEvents = function () {
|
|
$this->events = $this->loadNostrEvents([32122]);
|
|
};
|
|
|
|
$loadBoardEvents = function () {
|
|
$this->boardEvents = $this->loadNostrEvents([2121]);
|
|
};
|
|
|
|
$loadNostrEvents = function ($kinds) {
|
|
$subscription = new Subscription();
|
|
$subscriptionId = $subscription->setId();
|
|
$filter = new Filter();
|
|
$filter->setKinds($kinds);
|
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
|
$relaySet = new RelaySet();
|
|
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
|
$request = new Request($relaySet, $requestMessage);
|
|
$response = $request->send();
|
|
return collect($response[config('services.relay')])
|
|
->map(fn($event)
|
|
=> [
|
|
'id' => $event->event->id,
|
|
'kind' => $event->event->kind,
|
|
'content' => $event->event->content,
|
|
'pubkey' => $event->event->pubkey,
|
|
'tags' => $event->event->tags,
|
|
'created_at' => $event->event->created_at,
|
|
])->toArray();
|
|
};
|
|
|
|
$vote = function ($pubkey, $type, $board = false) {
|
|
if ($this->election->end_time?->isPast()) {
|
|
$this->isNotClosed = false;
|
|
return;
|
|
}
|
|
$note = new NostrEvent();
|
|
$note->setKind($board ? 2121 : 32122);
|
|
if (!$board) {
|
|
$dTag = sprintf('%s,%s,%s', $this->currentPleb->pubkey, date('Y'), $type);
|
|
$note->setTags([['d', $dTag]]);
|
|
}
|
|
$note->setContent("$pubkey,$type");
|
|
$this->signThisEvent = $note->toJson();
|
|
};
|
|
|
|
$checkElection = function () {
|
|
if ($this->election->end_time?->isPast()) {
|
|
$this->isNotClosed = false;
|
|
}
|
|
};
|
|
|
|
$signEvent = function ($event) {
|
|
$note = new NostrEvent();
|
|
$note->setId($event['id']);
|
|
$note->setSignature($event['sig']);
|
|
$note->setKind($event['kind']);
|
|
$note->setContent($event['content']);
|
|
$note->setPublicKey($event['pubkey']);
|
|
$note->setTags($event['tags']);
|
|
$note->setCreatedAt($event['created_at']);
|
|
$eventMessage = new EventMessage($note);
|
|
$relay = new Relay(config('services.relay'));
|
|
$relay->setMessage($eventMessage);
|
|
$relay->send();
|
|
Broadcast::on('votes')->as('newVote')->sendNow();
|
|
};
|
|
|
|
?>
|
|
|
|
<x-layouts.app
|
|
:seo="new \RalphJSmit\Laravel\SEO\Support\SEOData(title: 'Wahlen ' . $election->year, description: 'Wahlen des Vereins im Jahr ' . $election->year)"
|
|
>
|
|
@volt
|
|
<div x-cloak x-show="isAllowed" class="relative flex h-full" x-data="nostrApp(@this)"
|
|
wire:poll.600000ms="checkElection">
|
|
|
|
@php
|
|
$positions = [
|
|
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
|
'board' => ['icon' => 'fa-users', 'title' => 'Vizepräsidium'],
|
|
];
|
|
$loadedEvents = collect($events)
|
|
->map(function($event) {
|
|
$profile = \App\Models\Profile::query()
|
|
->where('pubkey', $event['pubkey'])
|
|
->first()
|
|
?->toArray();
|
|
$votedFor = \App\Models\Profile::query()
|
|
->where('pubkey', str($event['content'])->before(',')->toString())
|
|
->first()
|
|
?->toArray();
|
|
|
|
return [
|
|
'id' => $event['id'],
|
|
'kind' => $event['kind'],
|
|
'content' => $event['content'],
|
|
'pubkey' => $event['pubkey'],
|
|
'tags' => $event['tags'],
|
|
'created_at' => $event['created_at'],
|
|
'profile' => $profile,
|
|
'votedFor' => $votedFor,
|
|
'type' => str($event['content'])->after(',')->toString(),
|
|
];
|
|
})
|
|
->sortByDesc('created_at')
|
|
->unique(fn ($event) => $event['pubkey'] . $event['type'])
|
|
->values();
|
|
$loadedBoardEvents = collect($boardEvents)
|
|
->map(function($event) {
|
|
$profile = \App\Models\Profile::query()
|
|
->where('pubkey', $event['pubkey'])
|
|
->first()
|
|
?->toArray();
|
|
$votedFor = \App\Models\Profile::query()
|
|
->where('pubkey', str($event['content'])->before(',')->toString())
|
|
->first()
|
|
?->toArray();
|
|
|
|
return [
|
|
'id' => $event['id'],
|
|
'kind' => $event['kind'],
|
|
'content' => $event['content'],
|
|
'pubkey' => $event['pubkey'],
|
|
'tags' => $event['tags'],
|
|
'created_at' => $event['created_at'],
|
|
'profile' => $profile,
|
|
'votedFor' => $votedFor,
|
|
'type' => str($event['content'])->after(',')->toString(),
|
|
];
|
|
})
|
|
->sortByDesc('created_at')
|
|
->values();
|
|
@endphp
|
|
|
|
<!-- Inbox sidebar -->
|
|
<div id="inbox-sidebar"
|
|
class="absolute z-20 top-0 bottom-0 w-full md:w-auto md:static md:top-auto md:bottom-auto -mr-px md:translate-x-0 transition-transform duration-200 ease-in-out"
|
|
:class="inboxSidebarOpen ? 'translate-x-0' : '-translate-x-full'">
|
|
<div
|
|
class="sticky top-16 bg-white dark:bg-[#1B1B1B] overflow-x-hidden overflow-y-auto no-scrollbar shrink-0 border-r border-gray-200 dark:border-gray-700/60 md:w-[18rem] xl:w-[20rem] h-[calc(100dvh-64px)]">
|
|
|
|
<!-- #Marketing group -->
|
|
<div>
|
|
<!-- Group header -->
|
|
<div class="sticky top-0 z-10">
|
|
<div
|
|
class="flex items-center bg-white dark:bg-[#1B1B1B] border-b border-gray-200 dark:border-gray-700/60 px-5 h-16">
|
|
<div class="w-full flex items-center justify-between">
|
|
<!-- Channel menu -->
|
|
<div class="relative" x-data="{ open: false }">
|
|
<button class="grow flex items-center truncate" aria-haspopup="true"
|
|
@click.prevent="open = !open" :aria-expanded="open">
|
|
<div class="truncate">
|
|
<span
|
|
class="font-semibold text-gray-800 dark:text-gray-100">2024</span>
|
|
</div>
|
|
<svg class="w-3 h-3 shrink-0 ml-1 fill-current text-gray-400 dark:text-gray-500"
|
|
viewBox="0 0 12 12">
|
|
<path d="M5.9 11.4L.5 6l1.4-1.4 4 4 4-4L11.3 6z"/>
|
|
</svg>
|
|
</button>
|
|
<div
|
|
class="origin-top-right z-10 absolute top-full left-0 min-w-60 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700/60 py-1.5 rounded-lg shadow-lg overflow-hidden mt-1"
|
|
@click.outside="open = false" @keydown.escape.window="open = false"
|
|
x-show="open"
|
|
x-transition:enter="transition ease-out duration-200 transform"
|
|
x-transition:enter-start="opacity-0 -translate-y-2"
|
|
x-transition:enter-end="opacity-100 translate-y-0"
|
|
x-transition:leave="transition ease-out duration-200"
|
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
x-cloak>
|
|
<ul>
|
|
<li>
|
|
<a class="font-medium text-sm text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-200 block py-1.5 px-3"
|
|
href="#0" @click="open = false" @focus="open = true"
|
|
@focusout="open = false">
|
|
<div class="flex items-center justify-between">
|
|
<div class="grow flex items-center truncate">
|
|
<div class="truncate">2024</div>
|
|
</div>
|
|
<svg class="w-3 h-3 shrink-0 fill-current text-orange-500 ml-1"
|
|
viewBox="0 0 12 12">
|
|
<path
|
|
d="M10.28 1.28L3.989 7.575 1.695 5.28A1 1 0 00.28 6.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 1.28z"/>
|
|
</svg>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Group body -->
|
|
<div class="px-5 py-4">
|
|
<!-- Search form -->
|
|
<form class="relative">
|
|
<label for="inbox-search" class="sr-only">Search</label>
|
|
<input
|
|
wire:model.live.debounce="search"
|
|
id="inbox-search" class="form-input w-full pl-9 bg-white dark:bg-gray-800"
|
|
type="search" placeholder="Suche…"/>
|
|
<button class="absolute inset-0 right-auto group" type="submit" aria-label="Search">
|
|
<svg
|
|
class="shrink-0 fill-current text-gray-400 dark:text-gray-500 group-hover:text-gray-500 dark:group-hover:text-gray-400 ml-3 mr-2"
|
|
width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
<path
|
|
d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z"/>
|
|
<path
|
|
d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z"/>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
<!-- Inbox -->
|
|
<div class="mt-4">
|
|
<div class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase mb-3">
|
|
Plebs
|
|
</div>
|
|
<ul class="mb-6">
|
|
@foreach($plebs as $pleb)
|
|
<li class="-mx-2">
|
|
<div class="flex w-full p-2 rounded text-left">
|
|
<img class="w-8 h-8 rounded-full mr-2 bg-black"
|
|
src="{{ $pleb['profile']['picture'] ?? 'https://robohash.org/test' }}"
|
|
onerror="this.onerror=null; this.src='https://robohash.org/test';"
|
|
width="32"
|
|
height="32"
|
|
alt="A"/>
|
|
<div class="grow truncate">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<div class="truncate">
|
|
<span
|
|
class="text-sm font-semibold text-gray-800 dark:text-gray-100 truncate">{{ $pleb['profile']['name'] ?? $pleb['pubkey'] }}</span>
|
|
</div>
|
|
<div class="text-xs text-gray-500 font-medium">
|
|
<x-badge
|
|
color="{{ \App\Enums\AssociationStatus::from($pleb['association_status'])->color() }}"
|
|
label="{{ \App\Enums\AssociationStatus::from($pleb['association_status'])->label() }}"/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="text-xs font-medium text-gray-800 dark:text-gray-100 truncate mb-0.5">
|
|
<div class="flex items-center space-x-2 h-5">
|
|
@foreach($positions as $name => $p)
|
|
@php
|
|
$votedResult = $loadedEvents->filter(fn ($e) => $e['pubkey'] === $pleb['pubkey'])->firstWhere('type', $name);
|
|
@endphp
|
|
<div class="flex space-x-2" wire:key="p_{{ $name }}">
|
|
@if($votedResult)
|
|
<i class="fa-sharp-duotone fa-solid {{ $p['icon'] }} w-4 h-4 fill-current text-green-500"></i>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inbox body -->
|
|
@if($currentPubkey)
|
|
|
|
@php
|
|
$electionConfig = collect(json_decode($election->candidates, true, 512, JSON_THROW_ON_ERROR))
|
|
->map(function ($c) use ($loadedEvents, $currentPubkey) {
|
|
$candidates = \App\Models\Profile::query()
|
|
->whereIn('pubkey', $c['c'])
|
|
->get()
|
|
->map(function ($p) use ($loadedEvents, $c, $currentPubkey) {
|
|
$votedClass = ' bg-green-500/20 text-green-700';
|
|
$notVotedClass = ' bg-gray-500/20 text-gray-100';
|
|
$hasVoted = $loadedEvents
|
|
->filter(fn($e) => $e['type'] === $c['type'] && $e['pubkey'] === $currentPubkey)
|
|
->firstWhere('votedFor.pubkey', $p->pubkey);
|
|
|
|
return [
|
|
'pubkey' => $p->pubkey,
|
|
'name' => $p->name,
|
|
'picture' => $p->picture,
|
|
'votedClass' => $hasVoted ? $votedClass : $notVotedClass,
|
|
];
|
|
});
|
|
|
|
return [
|
|
'type' => $c['type'],
|
|
'c' => $c['c'],
|
|
'candidates' => $candidates,
|
|
];
|
|
});
|
|
$electionConfigBoard = collect(json_decode($election->candidates, true, 512, JSON_THROW_ON_ERROR))
|
|
->map(function ($c) use ($loadedBoardEvents, $currentPubkey) {
|
|
$candidates = \App\Models\Profile::query()
|
|
->whereIn('pubkey', $c['c'])
|
|
->get()
|
|
->map(function ($p) use ($loadedBoardEvents, $c, $currentPubkey) {
|
|
$votedClass = ' bg-green-500/20 text-green-700';
|
|
$notVotedClass = ' bg-gray-500/20 text-gray-100';
|
|
$hasVoted = $loadedBoardEvents
|
|
->filter(fn($e) => $e['type'] === $c['type'] && $e['pubkey'] === $currentPubkey)
|
|
->firstWhere('votedFor.pubkey', $p->pubkey);
|
|
|
|
return [
|
|
'pubkey' => $p->pubkey,
|
|
'name' => $p->name,
|
|
'picture' => $p->picture,
|
|
'votedClass' => $hasVoted ? $votedClass : $notVotedClass,
|
|
'hasVoted' => $hasVoted,
|
|
];
|
|
});
|
|
|
|
return [
|
|
'type' => $c['type'],
|
|
'c' => $c['c'],
|
|
'candidates' => $candidates,
|
|
];
|
|
});
|
|
@endphp
|
|
|
|
<div class="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out"
|
|
:class="inboxSidebarOpen ? 'translate-x-1/3' : 'translate-x-0'">
|
|
|
|
<!-- Header -->
|
|
<div class="sticky top-16">
|
|
<div
|
|
class="flex items-center justify-between before:absolute before:inset-0 before:backdrop-blur-md before:bg-gray-50/90 dark:before:bg-[#1B1B1B]/90 before:-z-10 border-b border-gray-200 dark:border-gray-700/60 px-4 sm:px-6 md:px-5 h-16">
|
|
<div
|
|
class="flex flex-col space-y-2 sm:space-y-0 sm:flex-row justify-between items-center w-full">
|
|
<div>
|
|
@if($isNotClosed)
|
|
<x-badge success
|
|
label="Die Wahl ist geöffnet bis zum {{ $election->end_time?->timezone('Europe/Berlin')->format('d.m.Y H:i') }}"/>
|
|
@else
|
|
<x-badge negative label="Die Wahl ist geschlossen"/>
|
|
@endif
|
|
</div>
|
|
<div>
|
|
<x-button secondary
|
|
:href="route('association.election.admin', ['election' => $election])"
|
|
label="Wahl-Admin"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="grow px-4 sm:px-6 md:px-5 py-4">
|
|
|
|
<!-- Mail subject -->
|
|
<header class="sm:flex sm:items-start space-x-4 mb-4">
|
|
<h1 class="text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
|
|
Wahlen
|
|
</h1>
|
|
<button
|
|
class="text-xs inline-flex font-bold bg-amber-500/20 text-sky-700 rounded-full text-center px-2.5 py-1 whitespace-nowrap">
|
|
2024
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Messages box -->
|
|
<div
|
|
class="shadow-sm rounded-xl px-6 divide-y divide-gray-200 dark:divide-gray-700/60">
|
|
|
|
<!-- Mail -->
|
|
<div class="py-6">
|
|
<h1 class="text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
|
|
Wahl des Präsidiums
|
|
</h1>
|
|
@php
|
|
$president = $positions['presidency'];
|
|
$board = $positions['board'];
|
|
@endphp
|
|
<div class="grid sm:grid-cols-2 gap-6">
|
|
<div
|
|
class="bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
|
<div class="flex flex-col h-full p-5">
|
|
<header>
|
|
<div class="flex items-center justify-between">
|
|
<i class="fa-sharp-duotone fa-solid {{ $president['icon'] }} w-9 h-9 fill-current text-white"></i>
|
|
</div>
|
|
</header>
|
|
<div class="grow mt-2">
|
|
<div
|
|
class="inline-flex text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:text-white mb-1">
|
|
<h2 class="text-xl leading-snug font-semibold">{{ $president['title'] }}</h2>
|
|
</div>
|
|
<div class="text-sm">
|
|
@php
|
|
$votedResult = $loadedEvents->filter(fn ($event) => $event['pubkey'] === $currentPubkey)->firstWhere('type', 'presidency');
|
|
@endphp
|
|
@if($votedResult)
|
|
<span>Du hast "{{ $votedResult['votedFor']['name'] ?? 'error' }}" gewählt</span>
|
|
@else
|
|
<span>Wähle deinen Kandidaten für das Präsidium.</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<footer class="mt-5">
|
|
<div class="grid sm:grid-cols-2 gap-2">
|
|
@foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] ?? [] as $c)
|
|
<div
|
|
@if($isNotClosed)wire:click="vote('{{ $c['pubkey'] }}', 'presidency')"
|
|
@endif
|
|
class="{{ $c['votedClass'] }} cursor-pointer text-xs inline-flex font-medium rounded-full text-center px-2.5 py-1">
|
|
<div class="flex items-center">
|
|
<img class="w-6 h-6 rounded-full mr-2 bg-black"
|
|
src="{{ $c['picture'] ?? 'https://robohash.org/' . $c['pubkey'] }}"
|
|
onerror="this.onerror=null; this.src='https://robohash.org/{{ $c['pubkey'] }}';"
|
|
width="24" height="24"
|
|
alt="{{ $c['name'] }}"/>
|
|
{{ $c['name'] }}
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h1 class="mt-6 text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
|
|
Wahl der übrigen Vorstandsmitglieder
|
|
</h1>
|
|
<div class="grid gap-6">
|
|
|
|
<div
|
|
class="bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
|
<div class="flex flex-col h-full p-5">
|
|
<div class="grow mt-2">
|
|
<div class="text-sm">
|
|
<span>Klicke auf den Kandidaten, um seine Position als Vorstandsmitglied zu bestätigen.</span>
|
|
</div>
|
|
</div>
|
|
<footer class="mt-5">
|
|
<div class="grid sm:grid-cols-4 gap-2">
|
|
@foreach($electionConfigBoard->firstWhere('type', 'board')['candidates'] ?? [] as $c)
|
|
<div
|
|
@if($isNotClosed && !$c['hasVoted'])wire:click="vote('{{ $c['pubkey'] }}', 'board', true)"
|
|
@endif
|
|
class="{{ $c['votedClass'] }} cursor-pointer text-xs inline-flex font-medium rounded-full text-center px-2.5 py-1">
|
|
<div class="flex items-center">
|
|
<img class="w-6 h-6 rounded-full mr-2 bg-black"
|
|
src="{{ $c['picture'] ?? 'https://robohash.org/' . $c['pubkey'] }}"
|
|
onerror="this.onerror=null; this.src='https://robohash.org/{{ $c['pubkey'] }}';"
|
|
width="24" height="24"
|
|
alt="{{ $c['name'] }}"/>
|
|
{{ $c['name'] }}
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Log events -->
|
|
<div x-cloak x-show="showLog" class="mt-6 hidden sm:block">
|
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-xl mb-8">
|
|
<header class="px-5 py-4">
|
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100">Präsidium Log <span
|
|
class="text-gray-400 dark:text-gray-500 font-medium">{{ $loadedEvents->count() }}</span>
|
|
</h2>
|
|
</header>
|
|
<div>
|
|
<!-- Table -->
|
|
<div class="overflow-x-auto">
|
|
<table
|
|
class="table-auto w-full dark:text-gray-300 divide-y divide-gray-100 dark:divide-gray-700/60">
|
|
<!-- Table header -->
|
|
<thead
|
|
class="text-xs uppercase text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900/20 border-t border-gray-100 dark:border-gray-700/60">
|
|
<tr>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">ID</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Kind</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Pubkey</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Created At</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Voted For</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Type</div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<!-- Table body -->
|
|
<tbody class="text-sm">
|
|
@foreach($loadedEvents as $event)
|
|
<tr>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div
|
|
class="font-medium">{{ \Illuminate\Support\Str::limit($event['id'], 10) }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['kind'] }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['profile']['name'] ?? '' }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['created_at'] }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['votedFor']['name'] ?? '' }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['type'] }}</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div x-cloak x-show="showLog" class="mt-6 hidden sm:block">
|
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-xl mb-8">
|
|
<header class="px-5 py-4">
|
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100">Board Log <span
|
|
class="text-gray-400 dark:text-gray-500 font-medium">{{ $loadedBoardEvents->count() }}</span>
|
|
</h2>
|
|
</header>
|
|
<div>
|
|
<!-- Table -->
|
|
<div class="overflow-x-auto">
|
|
<table
|
|
class="table-auto w-full dark:text-gray-300 divide-y divide-gray-100 dark:divide-gray-700/60">
|
|
<!-- Table header -->
|
|
<thead
|
|
class="text-xs uppercase text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900/20 border-t border-gray-100 dark:border-gray-700/60">
|
|
<tr>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">ID</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Kind</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Pubkey</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Created At</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Voted For</div>
|
|
</th>
|
|
<th class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div class="font-semibold text-left">Type</div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<!-- Table body -->
|
|
<tbody class="text-sm">
|
|
@foreach($loadedBoardEvents as $event)
|
|
<tr>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div
|
|
class="font-medium">{{ \Illuminate\Support\Str::limit($event['id'], 10) }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['kind'] }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['profile']['name'] ?? '' }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['created_at'] }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['votedFor']['name'] ?? '' }}</div>
|
|
</td>
|
|
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
|
<div>{{ $event['type'] }}</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
@endif
|
|
|
|
</div>
|
|
@endvolt
|
|
</x-layouts.app>
|