mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2025-12-14 06:36:46 +00:00
feat: add boardVotes and showLog functionalities
This commit includes the addition of a 'boardVotes' functionality in the electionAdminCharts.js file, and a 'showLog' functionality in the nostrApp.js file. This update provides enhanced interactivity and data handling for the charts and logs respectively.
This commit is contained in:
@@ -4,6 +4,7 @@ export default (livewireComponent) => ({
|
|||||||
plebs: livewireComponent.entangle('plebs', true),
|
plebs: livewireComponent.entangle('plebs', true),
|
||||||
electionConfig: livewireComponent.entangle('electionConfig', true),
|
electionConfig: livewireComponent.entangle('electionConfig', true),
|
||||||
votes: livewireComponent.entangle('votes', true),
|
votes: livewireComponent.entangle('votes', true),
|
||||||
|
boardVotes: livewireComponent.entangle('boardVotes', true),
|
||||||
charts: {}, // Store chart instances
|
charts: {}, // Store chart instances
|
||||||
|
|
||||||
hexToRGB(h) {
|
hexToRGB(h) {
|
||||||
@@ -24,19 +25,11 @@ export default (livewireComponent) => ({
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.createChart('chart_presidency', 'presidency');
|
this.createChart('chart_presidency', 'presidency');
|
||||||
this.createChart('chart_vice_president', 'vice_president');
|
this.createChart('chart_board', 'board');
|
||||||
this.createChart('chart_finances', 'finances');
|
|
||||||
this.createChart('chart_secretary', 'secretary');
|
|
||||||
this.createChart('chart_press_officer', 'press_officer');
|
|
||||||
this.createChart('chart_it_manager', 'it_manager');
|
|
||||||
|
|
||||||
this.$watch('votes', () => {
|
this.$watch('votes', () => {
|
||||||
this.createChart('chart_presidency', 'presidency');
|
this.createChart('chart_presidency', 'presidency');
|
||||||
this.createChart('chart_vice_president', 'vice_president');
|
this.createChart('chart_board', 'board');
|
||||||
this.createChart('chart_finances', 'finances');
|
|
||||||
this.createChart('chart_secretary', 'secretary');
|
|
||||||
this.createChart('chart_press_officer', 'press_officer');
|
|
||||||
this.createChart('chart_it_manager', 'it_manager');
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -79,7 +72,12 @@ export default (livewireComponent) => ({
|
|||||||
const config = this.electionConfig.find(config => config.type === type);
|
const config = this.electionConfig.find(config => config.type === type);
|
||||||
const labels = config ? config.candidates.map(candidate => candidate.name) : [];
|
const labels = config ? config.candidates.map(candidate => candidate.name) : [];
|
||||||
const labelsPubkeys = config ? config.candidates.map(candidate => candidate.pubkey) : [];
|
const labelsPubkeys = config ? config.candidates.map(candidate => candidate.pubkey) : [];
|
||||||
const data = this.votes.find(vote => vote.type === type);
|
let data;
|
||||||
|
if (type === 'board') {
|
||||||
|
data = this.boardVotes.find(vote => vote.type === type);
|
||||||
|
} else {
|
||||||
|
data = this.votes.find(vote => vote.type === type);
|
||||||
|
}
|
||||||
const findVoteCountInDataByLabelsPubkey = data ? labelsPubkeys.map(pubkey => data.votes[pubkey]?.count ?? 0) : labelsPubkeys.map(() => 0);
|
const findVoteCountInDataByLabelsPubkey = data ? labelsPubkeys.map(pubkey => data.votes[pubkey]?.count ?? 0) : labelsPubkeys.map(() => 0);
|
||||||
console.log('findVoteCountInDataByLabelsPubkey', findVoteCountInDataByLabelsPubkey);
|
console.log('findVoteCountInDataByLabelsPubkey', findVoteCountInDataByLabelsPubkey);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export default (livewireComponent) => ({
|
|||||||
|
|
||||||
isAllowed: livewireComponent.entangle('isAllowed', true),
|
isAllowed: livewireComponent.entangle('isAllowed', true),
|
||||||
signThisEvent: livewireComponent.entangle('signThisEvent'),
|
signThisEvent: livewireComponent.entangle('signThisEvent'),
|
||||||
|
showLog: livewireComponent.entangle('showLog', true),
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// on change of signThisEvent, call the method
|
// on change of signThisEvent, call the method
|
||||||
|
|||||||
@@ -1,49 +1,46 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Volt\Component;
|
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 swentel\nostr\Filter\Filter;
|
use function Livewire\Volt\{computed, mount, state, with, updated, on};
|
||||||
use swentel\nostr\Key\Key;
|
use function Laravel\Folio\{middleware, name};
|
||||||
use swentel\nostr\Message\EventMessage;
|
|
||||||
use swentel\nostr\Message\RequestMessage;
|
|
||||||
use swentel\nostr\Relay\Relay;
|
|
||||||
use swentel\nostr\Relay\RelaySet;
|
|
||||||
use swentel\nostr\Request\Request;
|
|
||||||
use swentel\nostr\Subscription\Subscription;
|
|
||||||
use swentel\nostr\Event\Event as NostrEvent;
|
|
||||||
use swentel\nostr\Sign\Sign;
|
|
||||||
|
|
||||||
use function Livewire\Volt\computed;
|
|
||||||
use function Livewire\Volt\mount;
|
|
||||||
use function Livewire\Volt\state;
|
|
||||||
use function Livewire\Volt\with;
|
|
||||||
use function Livewire\Volt\updated;
|
|
||||||
use function Laravel\Folio\{middleware};
|
|
||||||
use function Laravel\Folio\name;
|
|
||||||
use function Livewire\Volt\{on};
|
|
||||||
|
|
||||||
name('association.election');
|
name('association.election');
|
||||||
|
|
||||||
state(['isAllowed' => false]);
|
state([
|
||||||
state(['currentPubkey' => null]);
|
'isAllowed' => false,
|
||||||
state(['currentPleb' => null]);
|
'showLog' => false,
|
||||||
state(['events' => []]);
|
'currentPubkey' => null,
|
||||||
state(['election' => fn() => $election]);
|
'currentPleb' => null,
|
||||||
state(['plebs' => []]);
|
'events' => [],
|
||||||
state(['search' => '']);
|
'boardEvents' => [],
|
||||||
state(['signThisEvent' => '']);
|
'election' => fn() => $election,
|
||||||
state(['isNotClosed' => true]);
|
'plebs' => [],
|
||||||
|
'search' => '',
|
||||||
|
'signThisEvent' => '',
|
||||||
|
'isNotClosed' => true
|
||||||
|
]);
|
||||||
|
|
||||||
mount(function () {
|
mount(function () {
|
||||||
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
||||||
->with([
|
->with(['profile'])
|
||||||
'profile',
|
|
||||||
])
|
|
||||||
->whereIn('association_status', [3, 4])
|
->whereIn('association_status', [3, 4])
|
||||||
->orderBy('association_status', 'desc')
|
->orderBy('association_status', 'desc')
|
||||||
->get()
|
->get()
|
||||||
->toArray();
|
->toArray();
|
||||||
$this->loadEvents();
|
$this->loadEvents();
|
||||||
|
$this->loadBoardEvents();
|
||||||
if ($this->election->end_time->isPast() || !config('services.voting')) {
|
if ($this->election->end_time->isPast() || !config('services.voting')) {
|
||||||
$this->isNotClosed = false;
|
$this->isNotClosed = false;
|
||||||
}
|
}
|
||||||
@@ -52,60 +49,59 @@ mount(function () {
|
|||||||
on([
|
on([
|
||||||
'nostrLoggedIn' => function ($pubkey) {
|
'nostrLoggedIn' => function ($pubkey) {
|
||||||
$this->currentPubkey = $pubkey;
|
$this->currentPubkey = $pubkey;
|
||||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||||
->where('pubkey', $pubkey)->first();
|
|
||||||
if ($this->currentPleb->association_status->value < 3) {
|
if ($this->currentPleb->association_status->value < 3) {
|
||||||
return redirect()->route('association.profile');
|
return redirect()->route('association.profile');
|
||||||
}
|
}
|
||||||
|
$logPubkeys = [
|
||||||
|
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||||
|
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||||
|
];
|
||||||
|
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||||
|
$this->showLog = true;
|
||||||
|
}
|
||||||
$this->isAllowed = true;
|
$this->isAllowed = true;
|
||||||
},
|
},
|
||||||
]);
|
|
||||||
|
|
||||||
on([
|
|
||||||
'echo:votes,.newVote' => function () {
|
'echo:votes,.newVote' => function () {
|
||||||
$this->loadEvents();
|
$this->loadEvents();
|
||||||
|
$this->loadBoardEvents();
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
updated([
|
updated([
|
||||||
'search' => function ($value) {
|
'search' => function ($value) {
|
||||||
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
||||||
->with([
|
->with(['profile'])
|
||||||
'profile',
|
|
||||||
])
|
|
||||||
->whereIn('association_status', [3, 4])
|
->whereIn('association_status', [3, 4])
|
||||||
->where(fn($query)
|
->where(fn($query)
|
||||||
=> $query
|
=> $query
|
||||||
->where('pubkey', 'like', "%$value%")
|
->where('pubkey', 'like', "%$value%")
|
||||||
->orWhereHas('profile', function ($query) use ($value) {
|
->orWhereHas('profile', fn($query) => $query->where('name', 'ilike', "%$value%")))
|
||||||
$query->where('name', 'ilike', "%$value%");
|
|
||||||
}))
|
|
||||||
->orderBy('association_status', 'desc')
|
->orderBy('association_status', 'desc')
|
||||||
->get()
|
->get()
|
||||||
->toArray();
|
->toArray();
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$loadEvents = function () {
|
$loadEvents = function () {
|
||||||
|
$this->events = $this->loadNostrEvents([32121]);
|
||||||
|
};
|
||||||
|
|
||||||
|
$loadBoardEvents = function () {
|
||||||
|
$this->boardEvents = $this->loadNostrEvents([2121]);
|
||||||
|
};
|
||||||
|
|
||||||
|
$loadNostrEvents = function ($kinds) {
|
||||||
$subscription = new Subscription();
|
$subscription = new Subscription();
|
||||||
$subscriptionId = $subscription->setId();
|
$subscriptionId = $subscription->setId();
|
||||||
|
$filter = new Filter();
|
||||||
$filter1 = new Filter();
|
$filter->setKinds($kinds);
|
||||||
$filter1->setKinds([2121]); // You can add multiple kind numbers
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
||||||
$filters = [$filter1]; // You can add multiple filters.
|
|
||||||
|
|
||||||
$requestMessage = new RequestMessage($subscriptionId, $filters);
|
|
||||||
|
|
||||||
$relays = [
|
|
||||||
new Relay(config('services.relay')),
|
|
||||||
];
|
|
||||||
$relaySet = new RelaySet();
|
$relaySet = new RelaySet();
|
||||||
$relaySet->setRelays($relays);
|
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
||||||
|
|
||||||
$request = new Request($relaySet, $requestMessage);
|
$request = new Request($relaySet, $requestMessage);
|
||||||
$response = $request->send();
|
$response = $request->send();
|
||||||
|
return collect($response[config('services.relay')])
|
||||||
$this->events = collect($response[config('services.relay')])
|
|
||||||
->map(fn($event)
|
->map(fn($event)
|
||||||
=> [
|
=> [
|
||||||
'id' => $event->event->id,
|
'id' => $event->event->id,
|
||||||
@@ -117,14 +113,18 @@ $loadEvents = function () {
|
|||||||
])->toArray();
|
])->toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
$vote = function ($pubkey, $type) {
|
$vote = function ($pubkey, $type, $board = false) {
|
||||||
if ($this->election->end_time->isPast()) {
|
if ($this->election->end_time->isPast()) {
|
||||||
$this->isNotClosed = false;
|
$this->isNotClosed = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$note = new NostrEvent();
|
$note = new NostrEvent();
|
||||||
$note->setContent($pubkey . ',' . $type);
|
$note->setKind($board ? 2121 : 32121);
|
||||||
$note->setKind(2121);
|
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();
|
$this->signThisEvent = $note->toJson();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,30 +144,23 @@ $signEvent = function ($event) {
|
|||||||
$note->setTags($event['tags']);
|
$note->setTags($event['tags']);
|
||||||
$note->setCreatedAt($event['created_at']);
|
$note->setCreatedAt($event['created_at']);
|
||||||
$eventMessage = new EventMessage($note);
|
$eventMessage = new EventMessage($note);
|
||||||
$relayUrl = config('services.relay');
|
$relay = new Relay(config('services.relay'));
|
||||||
$relay = new Relay($relayUrl);
|
|
||||||
$relay->setMessage($eventMessage);
|
$relay->setMessage($eventMessage);
|
||||||
$result = $relay->send();
|
$relay->send();
|
||||||
|
Broadcast::on('votes')->as('newVote')->sendNow();
|
||||||
Broadcast::on('votes')
|
|
||||||
->as('newVote')
|
|
||||||
->sendNow();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<x-layouts.app title="{{ __('Wahl') }}">
|
<x-layouts.app title="{{ __('Wahl') }}">
|
||||||
@volt
|
@volt
|
||||||
<div x-cloak x-show="isAllowed" class="relative flex h-full" x-data="nostrApp(@this)" wire:poll.600000ms="checkElection">
|
<div x-cloak x-show="isAllowed" class="relative flex h-full" x-data="nostrApp(@this)"
|
||||||
|
wire:poll.600000ms="checkElection">
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$positions = [
|
$positions = [
|
||||||
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
||||||
'vice_president' => ['icon' => 'fa-user-group-crown', 'title' => 'Vizepräsidium'],
|
'board' => ['icon' => 'fa-users', 'title' => 'Vizepräsidium'],
|
||||||
'finances' => ['icon' => 'fa-bitcoin-sign', 'title' => 'Finanzen'],
|
|
||||||
'secretary' => ['icon' => 'fa-stapler', 'title' => 'Revisionsstelle'],
|
|
||||||
'press_officer' => ['icon' => 'fa-newspaper', 'title' => 'Pressewart'],
|
|
||||||
'it_manager' => ['icon' => 'fa-server', 'title' => 'Technikwart'],
|
|
||||||
];
|
];
|
||||||
$loadedEvents = collect($events)
|
$loadedEvents = collect($events)
|
||||||
->map(function($event) {
|
->map(function($event) {
|
||||||
@@ -195,6 +188,31 @@ $signEvent = function ($event) {
|
|||||||
->sortByDesc('created_at')
|
->sortByDesc('created_at')
|
||||||
->unique(fn ($event) => $event['pubkey'] . $event['type'])
|
->unique(fn ($event) => $event['pubkey'] . $event['type'])
|
||||||
->values();
|
->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
|
@endphp
|
||||||
|
|
||||||
<!-- Inbox sidebar -->
|
<!-- Inbox sidebar -->
|
||||||
@@ -361,6 +379,33 @@ $signEvent = function ($event) {
|
|||||||
'candidates' => $candidates,
|
'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
|
@endphp
|
||||||
|
|
||||||
<div class="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out"
|
<div class="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out"
|
||||||
@@ -414,9 +459,11 @@ $signEvent = function ($event) {
|
|||||||
</h1>
|
</h1>
|
||||||
@php
|
@php
|
||||||
$president = $positions['presidency'];
|
$president = $positions['presidency'];
|
||||||
|
$board = $positions['board'];
|
||||||
@endphp
|
@endphp
|
||||||
|
<div class="grid sm:grid-cols-2 gap-6">
|
||||||
<div
|
<div
|
||||||
class="col-span-full sm:col-span-6 xl:col-span-4 bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
class="bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||||
<div class="flex flex-col h-full p-5">
|
<div class="flex flex-col h-full p-5">
|
||||||
<header>
|
<header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -440,7 +487,7 @@ $signEvent = function ($event) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="mt-5">
|
<footer class="mt-5">
|
||||||
<div class="grid sm:grid-cols-2 gap-y-2">
|
<div class="grid sm:grid-cols-2 gap-2">
|
||||||
@foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] as $c)
|
@foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] as $c)
|
||||||
<div
|
<div
|
||||||
@if($isNotClosed)wire:click="vote('{{ $c['pubkey'] }}', 'presidency')"
|
@if($isNotClosed)wire:click="vote('{{ $c['pubkey'] }}', 'presidency')"
|
||||||
@@ -460,43 +507,26 @@ $signEvent = function ($event) {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="mt-6 border-t text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
|
<h1 class="mt-6 text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
|
||||||
Bestätigung der Vorstandsmitglieder
|
Wahl der übrigen Vorstandsmitglieder
|
||||||
</h1>
|
</h1>
|
||||||
<div class="grid grid-cols-12 gap-6">
|
<div class="grid gap-6">
|
||||||
|
|
||||||
@foreach(collect($positions)->filter(fn($position, $type) => $type !== 'presidency') as $type => $position)
|
|
||||||
@if($electionConfig->firstWhere('type', $type))
|
|
||||||
<div
|
<div
|
||||||
class="col-span-full sm:col-span-6 xl:col-span-4 bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
class="bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||||
<div class="flex flex-col h-full p-5">
|
<div class="flex flex-col h-full p-5">
|
||||||
<header>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<i class="fa-sharp-duotone fa-solid {{ $position['icon'] }} w-9 h-9 fill-current text-white"></i>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="grow mt-2">
|
<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">{{ $position['title'] }}</h2>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
@php
|
|
||||||
$votedResult = $loadedEvents->filter(fn ($event) => $event['pubkey'] === $currentPubkey)->firstWhere('type', $type);
|
|
||||||
@endphp
|
|
||||||
@if($votedResult)
|
|
||||||
<span>Du hast "{{ $votedResult['votedFor']['name'] ?? 'error' }}" gewählt</span>
|
|
||||||
@else
|
|
||||||
<span>Klicke auf den Kandidaten, um seine Position als Vorstandsmitglied zu bestätigen.</span>
|
<span>Klicke auf den Kandidaten, um seine Position als Vorstandsmitglied zu bestätigen.</span>
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="mt-5">
|
<footer class="mt-5">
|
||||||
<div class="grid sm:grid-cols-2 gap-y-2">
|
<div class="grid sm:grid-cols-4 gap-2">
|
||||||
@foreach($electionConfig->firstWhere('type', $type)['candidates'] as $c)
|
@foreach($electionConfigBoard->firstWhere('type', 'board')['candidates'] as $c)
|
||||||
<div
|
<div
|
||||||
@if($isNotClosed)wire:click="vote('{{ $c['pubkey'] }}', '{{ $type }}')"
|
@if($isNotClosed && !$c['hasVoted'])wire:click="vote('{{ $c['pubkey'] }}', 'board', true)"
|
||||||
@endif
|
@endif
|
||||||
class="{{ $c['votedClass'] }} cursor-pointer text-xs inline-flex font-medium rounded-full text-center px-2.5 py-1">
|
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">
|
<div class="flex items-center">
|
||||||
@@ -513,8 +543,6 @@ $signEvent = function ($event) {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -522,10 +550,10 @@ $signEvent = function ($event) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log events -->
|
<!-- Log events -->
|
||||||
{{--<div class="mt-6 hidden sm:block">
|
<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">
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-xl mb-8">
|
||||||
<header class="px-5 py-4">
|
<header class="px-5 py-4">
|
||||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100">Logged Votes on Nostr <span
|
<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>
|
class="text-gray-400 dark:text-gray-500 font-medium">{{ $loadedEvents->count() }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
@@ -588,7 +616,74 @@ $signEvent = function ($event) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,45 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Volt\Component;
|
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 swentel\nostr\Filter\Filter;
|
use function Livewire\Volt\{computed, mount, state, with, updated, on};
|
||||||
use swentel\nostr\Key\Key;
|
use function Laravel\Folio\{middleware, name};
|
||||||
use swentel\nostr\Message\EventMessage;
|
|
||||||
use swentel\nostr\Message\RequestMessage;
|
|
||||||
use swentel\nostr\Relay\Relay;
|
|
||||||
use swentel\nostr\Relay\RelaySet;
|
|
||||||
use swentel\nostr\Request\Request;
|
|
||||||
use swentel\nostr\Subscription\Subscription;
|
|
||||||
use swentel\nostr\Event\Event as NostrEvent;
|
|
||||||
use swentel\nostr\Sign\Sign;
|
|
||||||
|
|
||||||
use function Livewire\Volt\computed;
|
|
||||||
use function Livewire\Volt\mount;
|
|
||||||
use function Livewire\Volt\state;
|
|
||||||
use function Livewire\Volt\with;
|
|
||||||
use function Livewire\Volt\updated;
|
|
||||||
use function Laravel\Folio\{middleware};
|
|
||||||
use function Laravel\Folio\name;
|
|
||||||
use function Livewire\Volt\{on};
|
|
||||||
|
|
||||||
name('association.election.admin');
|
name('association.election.admin');
|
||||||
|
|
||||||
state(['isAllowed' => false]);
|
|
||||||
state(['currentPubkey' => null]);
|
|
||||||
state(['votes' => null]);
|
|
||||||
state(['events' => null]);
|
|
||||||
state(['election' => fn() => $election]);
|
|
||||||
state(['signThisEvent' => '']);
|
|
||||||
state([
|
state([
|
||||||
|
'isAllowed' => false,
|
||||||
|
'currentPubkey' => null,
|
||||||
|
'votes' => null,
|
||||||
|
'boardVotes' => null,
|
||||||
|
'events' => null,
|
||||||
|
'boardEvents' => null,
|
||||||
|
'election' => fn() => $election,
|
||||||
|
'signThisEvent' => '',
|
||||||
'plebs' => fn()
|
'plebs' => fn()
|
||||||
=> \App\Models\EinundzwanzigPleb::query()
|
=> \App\Models\EinundzwanzigPleb::query()
|
||||||
->with([
|
->with(['profile'])
|
||||||
'profile',
|
|
||||||
])
|
|
||||||
->whereIn('association_status', [3, 4])
|
->whereIn('association_status', [3, 4])
|
||||||
->orderBy('association_status', 'desc')
|
->orderBy('association_status', 'desc')
|
||||||
->get()
|
->get()
|
||||||
->toArray(),
|
->toArray(),
|
||||||
]);
|
'electionConfig' => fn()
|
||||||
state([
|
=> collect(json_decode($this->election->candidates, true, 512, JSON_THROW_ON_ERROR))
|
||||||
'electionConfig' => function () {
|
->map(fn($c)
|
||||||
return collect(json_decode($this->election->candidates, true, 512, JSON_THROW_ON_ERROR))
|
=> [
|
||||||
->map(function ($c) {
|
'type' => $c['type'],
|
||||||
$candidates = \App\Models\Profile::query()
|
'c' => $c['c'],
|
||||||
|
'candidates' => \App\Models\Profile::query()
|
||||||
->whereIn('pubkey', $c['c'])
|
->whereIn('pubkey', $c['c'])
|
||||||
->get()
|
->get()
|
||||||
->map(fn($p)
|
->map(fn($p)
|
||||||
@@ -53,105 +47,111 @@ state([
|
|||||||
'pubkey' => $p->pubkey,
|
'pubkey' => $p->pubkey,
|
||||||
'name' => $p->name,
|
'name' => $p->name,
|
||||||
'picture' => $p->picture,
|
'picture' => $p->picture,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
mount(fn()
|
||||||
'type' => $c['type'],
|
=> [
|
||||||
'c' => $c['c'],
|
$this->loadEvents(),
|
||||||
'candidates' => $candidates,
|
$this->loadBoardEvents(),
|
||||||
];
|
$this->loadVotes(),
|
||||||
});
|
$this->loadBoardVotes(),
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mount(function () {
|
|
||||||
$this->loadEvents();
|
|
||||||
$this->loadVotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
on([
|
on([
|
||||||
'nostrLoggedIn' => function ($pubkey) {
|
'nostrLoggedIn' => fn($pubkey)
|
||||||
$this->currentPubkey = $pubkey;
|
=> [
|
||||||
|
$this->currentPubkey = $pubkey,
|
||||||
$allowedPubkeys = [
|
$allowedPubkeys = [
|
||||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279'
|
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||||
];
|
],
|
||||||
if (!in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
!in_array($this->currentPubkey, $allowedPubkeys, true) ? redirect()->route(
|
||||||
return redirect()->route('association.profile');
|
'association.profile',
|
||||||
}
|
) : $this->isAllowed = true,
|
||||||
$this->isAllowed = true;
|
],
|
||||||
},
|
'echo:votes,.newVote' => fn()
|
||||||
]);
|
=> [
|
||||||
|
$this->loadEvents(),
|
||||||
on([
|
$this->loadBoardEvents(),
|
||||||
'echo:votes,.newVote' => function () {
|
$this->loadVotes(),
|
||||||
$this->loadEvents();
|
$this->loadBoardVotes(),
|
||||||
$this->loadVotes();
|
],
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$loadVotes = function () {
|
$loadVotes = function () {
|
||||||
$votes = collect($this->events)
|
$this->votes = collect($this->events)
|
||||||
->map(function ($event) {
|
->map(fn($event)
|
||||||
$votedFor = \App\Models\Profile::query()
|
=> [
|
||||||
->where('pubkey', str($event['content'])->before(',')->toString())
|
|
||||||
->first();
|
|
||||||
if (!$votedFor) {
|
|
||||||
Artisan::call(\App\Console\Commands\Nostr\FetchProfile::class, [
|
|
||||||
'--pubkey' => str($event['content'])->before(',')->toString(),
|
|
||||||
]);
|
|
||||||
$votedFor = \App\Models\Profile::query()
|
|
||||||
->where('pubkey', str($event['content'])->before(',')->toString())
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
$votedFor = $votedFor->toArray();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'created_at' => $event['created_at'],
|
'created_at' => $event['created_at'],
|
||||||
'pubkey' => $event['pubkey'],
|
'pubkey' => $event['pubkey'],
|
||||||
'forpubkey' => $votedFor['pubkey'],
|
'forpubkey' => $this->fetchProfile($event['content']),
|
||||||
'type' => str($event['content'])->after(',')->toString(),
|
'type' => str($event['content'])->after(',')->toString(),
|
||||||
];
|
])
|
||||||
})
|
|
||||||
->sortByDesc('created_at')
|
->sortByDesc('created_at')
|
||||||
->unique(fn($event) => $event['pubkey'] . $event['type'])
|
->unique(fn($event) => $event['pubkey'] . $event['type'])
|
||||||
->values()
|
->values()
|
||||||
->toArray();
|
|
||||||
|
|
||||||
$this->votes = collect($votes)
|
|
||||||
->groupBy('type')
|
->groupBy('type')
|
||||||
->map(fn($votes)
|
->map(fn($votes)
|
||||||
=> [
|
=> [
|
||||||
'type' => $votes[0]['type'],
|
'type' => $votes[0]['type'],
|
||||||
'votes' => collect($votes)
|
'votes' => $votes->groupBy('forpubkey')->map(fn($group) => ['count' => $group->count()])->toArray(),
|
||||||
->groupBy('forpubkey')
|
])
|
||||||
->map(fn($group) => ['count' => $group->count()])
|
->values()
|
||||||
->toArray(),
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
$loadBoardVotes = function () {
|
||||||
|
$this->boardVotes = collect($this->boardEvents)
|
||||||
|
->map(fn($event)
|
||||||
|
=> [
|
||||||
|
'created_at' => $event['created_at'],
|
||||||
|
'pubkey' => $event['pubkey'],
|
||||||
|
'forpubkey' => $this->fetchProfile($event['content']),
|
||||||
|
'type' => str($event['content'])->after(',')->toString(),
|
||||||
|
])
|
||||||
|
->sortByDesc('created_at')
|
||||||
|
->values()
|
||||||
|
->groupBy('type')
|
||||||
|
->map(fn($votes)
|
||||||
|
=> [
|
||||||
|
'type' => $votes[0]['type'],
|
||||||
|
'votes' => $votes->groupBy('forpubkey')->map(fn($group) => ['count' => $group->count()])->toArray(),
|
||||||
])
|
])
|
||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
$loadEvents = function () {
|
$loadEvents = function () {
|
||||||
|
$this->events = $this->loadNostrEvents([32121]);
|
||||||
|
};
|
||||||
|
|
||||||
|
$loadBoardEvents = function () {
|
||||||
|
$this->boardEvents = $this->loadNostrEvents([2121]);
|
||||||
|
};
|
||||||
|
|
||||||
|
$fetchProfile = function ($content) {
|
||||||
|
$pubkey = str($content)->before(',')->toString();
|
||||||
|
$profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first();
|
||||||
|
if (!$profile) {
|
||||||
|
Artisan::call(\App\Console\Commands\Nostr\FetchProfile::class, ['--pubkey' => $pubkey]);
|
||||||
|
$profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first();
|
||||||
|
}
|
||||||
|
return $profile->pubkey;
|
||||||
|
};
|
||||||
|
|
||||||
|
$loadNostrEvents = function ($kinds) {
|
||||||
$subscription = new Subscription();
|
$subscription = new Subscription();
|
||||||
$subscriptionId = $subscription->setId();
|
$subscriptionId = $subscription->setId();
|
||||||
|
$filter = new Filter();
|
||||||
$filter1 = new Filter();
|
$filter->setKinds($kinds);
|
||||||
$filter1->setKinds([2121]); // You can add multiple kind numbers
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
||||||
$filters = [$filter1]; // You can add multiple filters.
|
|
||||||
|
|
||||||
$requestMessage = new RequestMessage($subscriptionId, $filters);
|
|
||||||
|
|
||||||
$relays = [
|
|
||||||
new Relay(config('services.relay')),
|
|
||||||
];
|
|
||||||
$relaySet = new RelaySet();
|
$relaySet = new RelaySet();
|
||||||
$relaySet->setRelays($relays);
|
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
||||||
|
|
||||||
$request = new Request($relaySet, $requestMessage);
|
$request = new Request($relaySet, $requestMessage);
|
||||||
$response = $request->send();
|
$response = $request->send();
|
||||||
|
return collect($response[config('services.relay')])
|
||||||
$this->events = collect($response[config('services.relay')])
|
|
||||||
->map(fn($event)
|
->map(fn($event)
|
||||||
=> [
|
=> [
|
||||||
'id' => $event->event->id,
|
'id' => $event->event->id,
|
||||||
@@ -160,8 +160,7 @@ $loadEvents = function () {
|
|||||||
'pubkey' => $event->event->pubkey,
|
'pubkey' => $event->event->pubkey,
|
||||||
'tags' => $event->event->tags,
|
'tags' => $event->event->tags,
|
||||||
'created_at' => $event->event->created_at,
|
'created_at' => $event->event->created_at,
|
||||||
])
|
])->toArray();
|
||||||
->toArray();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
?>
|
?>
|
||||||
@@ -171,11 +170,7 @@ $loadEvents = function () {
|
|||||||
@php
|
@php
|
||||||
$positions = [
|
$positions = [
|
||||||
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
||||||
'vice_president' => ['icon' => 'fa-user-group-crown', 'title' => 'Vizepräsidium'],
|
'board' => ['icon' => 'fa-users', 'title' => 'Vorstandsmitglieder'],
|
||||||
'finances' => ['icon' => 'fa-bitcoin-sign', 'title' => 'Finanzen'],
|
|
||||||
'secretary' => ['icon' => 'fa-stapler', 'title' => 'Revisionsstelle'],
|
|
||||||
'press_officer' => ['icon' => 'fa-newspaper', 'title' => 'Pressewart'],
|
|
||||||
'it_manager' => ['icon' => 'fa-server', 'title' => 'Technikwart'],
|
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@@ -193,24 +188,39 @@ $loadEvents = function () {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$president = $positions['presidency'];
|
||||||
|
$board = $positions['board'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
<!-- Cards -->
|
<!-- Cards -->
|
||||||
<div class="grid grid-cols-12 gap-6">
|
<div class="grid gap-y-4">
|
||||||
@foreach($positions as $key => $position)
|
<div wire:key="presidency" wire:ignore
|
||||||
<div wire:key="pos_{{ $key }}" wire:ignore
|
class="flex flex-col bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||||
class="flex flex-col col-span-full sm:col-span-6 bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
|
||||||
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
||||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
||||||
class="fa-sharp-duotone fa-solid {{ $position['icon'] }} w-5 h-5 fill-current text-white mr-4"></i>{{ $position['title'] }}
|
class="fa-sharp-duotone fa-solid {{ $president['icon'] }} w-5 h-5 fill-current text-white mr-4"></i>{{ $president['title'] }}
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<!-- Change the height attribute to adjust the chart height -->
|
<!-- Change the height attribute to adjust the chart height -->
|
||||||
<canvas x-ref="chart_{{ $key }}" width="724" height="288"
|
<canvas x-ref="chart_presidency" width="724" height="288"
|
||||||
|
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div wire:key="board" wire:ignore
|
||||||
|
class="flex flex-col bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||||
|
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
||||||
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
||||||
|
class="fa-sharp-duotone fa-solid {{ $board['icon'] }} w-5 h-5 fill-current text-white mr-4"></i>{{ $board['title'] }}
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<div class="grow">
|
||||||
|
<!-- Change the height attribute to adjust the chart height -->
|
||||||
|
<canvas x-ref="chart_board" width="724" height="288"
|
||||||
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user