Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,47 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
{{ __('Polls') }}
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-theme-background overflow-x-visible shadow-xl sm:rounded-lg">
|
||||
|
||||
<div class="p-12 px-6 sm:px-20 bg-theme-background">
|
||||
|
||||
@livewire('side-post', [
|
||||
'type' => 'SiteContents\Manage\Polls',
|
||||
'random' => false,
|
||||
'fallbackTitle' => __('Manage polls'),
|
||||
'fallbackDescription' => __('Create polls, set their questions, settings and target audience, and follow the results.') ])
|
||||
|
||||
@livewire('polls.manage')
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Date pickers live in modals that appear after page load: initialize new ones after each Livewire update
|
||||
function initPollDatePickers() {
|
||||
if (!window.LaravelFlatpickr) {
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('input.flatpickr-input[data-selector-id]').forEach(input => {
|
||||
if (!input._flatpickr) {
|
||||
window.LaravelFlatpickr.initializeFlatpickr(input);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setTimeout(initPollDatePickers, 100);
|
||||
Livewire.hook('morph.updated', () => {
|
||||
setTimeout(initPollDatePickers, 200);
|
||||
setTimeout(initPollDatePickers, 500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,107 @@
|
||||
@php
|
||||
$layout = Auth::check() ? 'app-layout' : 'guest-layout';
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$layout">
|
||||
<x-slot name="header">
|
||||
{{ $category }}
|
||||
</x-slot>
|
||||
|
||||
<div class="py-0 md:py-12" id="custom-timebank-style">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden bg-theme-background shadow-xl sm:rounded-lg">
|
||||
<div class="border-b border-theme-primary bg-theme-background p-6 sm:px-20 lg:px-32 lg:py-18">
|
||||
@auth
|
||||
<livewire:posts.manage-actions :post="$post" />
|
||||
@endauth
|
||||
<div class="mb-2 mt-0 md:mt-6 lg:mt-12">
|
||||
<div class="text-xs lg:text-sm text-theme-secondary">{{ $update }}</div>
|
||||
</div>
|
||||
<h2 class="mt-4 lg:mt-8 text-3xl lg:text-5xl font-semibold leading-tight text-theme-primary">
|
||||
{{ $translation->title }}
|
||||
</h2>
|
||||
|
||||
@if ($post->poll->closes_at && !$post->poll->isClosed())
|
||||
<h3 class="mt-4 lg:mt-8 text-xl lg:text-2xl font-semibold leading-tight text-theme-primary">
|
||||
{{ __('Open until :date', ['date' => $post->poll->closes_at->locale(App::getLocale())->isoFormat('LLL')]) }}
|
||||
</h3>
|
||||
@endif
|
||||
|
||||
@if ($translation->excerpt)
|
||||
<div class="mt-6">
|
||||
<div class="px-0 py-2 text-xl lg:text-2xl leading-normal lg:leading-loose font-bold text-theme-primary">
|
||||
{{ $translation->excerpt }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Image -->
|
||||
@if ($media)
|
||||
<div class="my-4 lg:my-12">
|
||||
{{ $media('hero', ['class' => 'w-full h-auto']) }}
|
||||
@php
|
||||
$imageCaption = $media->getCustomProperty('caption-' . App::getLocale());
|
||||
if (!$imageCaption) {
|
||||
foreach (array_keys(config('laravellocalization.supportedLocales')) as $fallbackLocale) {
|
||||
$imageCaption = $media->getCustomProperty('caption-' . $fallbackLocale);
|
||||
if ($imageCaption) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$captionText = implode(' ', array_filter([$imageCaption, $media->getCustomProperty('owner')]));
|
||||
@endphp
|
||||
@if ($captionText)
|
||||
<div class="mt-1 lg:mt-2 text-right text-3xs lg:text-xs text-theme-secondary">
|
||||
{{ $captionText }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($translation->content)
|
||||
<div class="post-content mt-2 lg:mt-6 mb-12 lg:mb-16 lg:mx-32 text-base lg:text-lg leading-relaxed md:leading-loose lg:leading-loose text-theme-primary">
|
||||
{!! \App\Helpers\StringHelper::sanitizeHtml($translation->content) !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Poll -->
|
||||
<div class="my-12 bg-theme-surface rounded-lg p-6 md:mx-32 border">
|
||||
@auth
|
||||
@if ($switchProfiles)
|
||||
{{-- The active profile may not take part, another of the user's profiles may --}}
|
||||
<div class="space-y-3">
|
||||
<p class="text-base text-theme-primary">{{ __('This poll is for another of your profiles. Switch to take part:') }}</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@foreach ($switchProfiles as $switchProfile)
|
||||
<x-jetstream.button onclick="window.location.href = '{{ $switchProfile['url'] }}'" class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('Take part as :name', ['name' => $switchProfile['name']]) }}
|
||||
</x-jetstream.button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<livewire:polls.poll-form :pollId="$post->poll->id" context="page" :wire:key="'poll-page-'.$post->poll->id" />
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@auth
|
||||
@if ($post->poll->verification !== 'off' && $post->poll->status !== 'draft')
|
||||
<p class="mb-6 text-sm text-theme-secondary md:mx-32">
|
||||
<a href="{{ route('polls.verify', $post->poll) }}" class="underline">{{ __('Verify this poll') }}</a>
|
||||
@if ($post->poll->sealed_at)
|
||||
· {{ __('Sealed') }}: <code class="font-mono text-xs break-all">{{ substr($post->poll->seal_hash, 0, 16) }}…</code>
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
<!-- Share -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{!! Share::mastodon()->bluesky()->linkedin()->instagram()->facebook()->whatsapp()->text($translation->title)->render() !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,11 @@
|
||||
{{-- Result bar. Expects: $label, $count, $total; optional $highlight, $suffix --}}
|
||||
@php($percentage = $total > 0 ? round($count / $total * 100) : 0)
|
||||
<div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-theme-primary {{ !empty($highlight) ? 'font-semibold' : '' }}">{{ $label }}</span>
|
||||
<span class="text-theme-secondary">{{ $suffix ?? $count . ' (' . $percentage . '%)' }}</span>
|
||||
</div>
|
||||
<div class="mt-1 h-2 w-full rounded bg-theme-surface border border-theme-border">
|
||||
<div class="h-full rounded bg-theme-brand" style="width: {{ $percentage }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
{{-- Checkbox with label. Expects: $model, $label; optional $disabled --}}
|
||||
<label @class(['flex items-center space-x-3', 'opacity-60' => $disabled ?? false])>
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model="{{ $model }}"
|
||||
@disabled($disabled ?? false)
|
||||
class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50"
|
||||
>
|
||||
<span class="text-sm text-gray-700">{{ $label }}</span>
|
||||
</label>
|
||||
@@ -0,0 +1,24 @@
|
||||
{{--
|
||||
Date (and time) picker, the same flatpickr input as the meeting dates in Posts/Manage.
|
||||
Expects: $model (wire:model path), $key (unique wire:key); optional $withTime (default true).
|
||||
Sends "Y-m-d H:i" (or "Y-m-d" without time). Initialized by the script in polls/manage.blade.php.
|
||||
--}}
|
||||
<div wire:ignore wire:key="{{ $key }}">
|
||||
@if ($withTime ?? true)
|
||||
<x-flatpickr
|
||||
showTime
|
||||
dateFormat="Y-m-d"
|
||||
timeFormat="H:i"
|
||||
altFormat="d-m-Y @ H:i"
|
||||
placeholder="{{ __('Select a date and time') }}"
|
||||
wire:model.defer="{{ $model }}"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" />
|
||||
@else
|
||||
<x-flatpickr
|
||||
dateFormat="Y-m-d"
|
||||
altFormat="d-m-Y"
|
||||
placeholder="{{ __('Select a date') }}"
|
||||
wire:model.defer="{{ $model }}"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" />
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
{{-- Validation message for one attribute. Expects: $field --}}
|
||||
@error($field)
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@@ -0,0 +1,39 @@
|
||||
{{-- Options list editor. Expects: $settings, $prefix, $index, $locale --}}
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">
|
||||
{{ __('Options') }} ({{ strtoupper($locale) }})
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
@foreach ($settings['options'] ?? [] as $i => $option)
|
||||
<div class="flex items-center space-x-2" wire:key="poll-q{{ $index }}-option-{{ $option['key'] }}">
|
||||
<div class="flex-1">
|
||||
<x-input
|
||||
wire:model="{{ $prefix }}.options.{{ $i }}.label.{{ $locale }}"
|
||||
type="text"
|
||||
placeholder="{{ __('Option') }} {{ $i + 1 }}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<x-jetstream.light-button wire:click="moveListItem({{ $index }}, 'options', {{ $i }}, -1)" :disabled="$i === 0" title="{{ __('Move up') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-up" />
|
||||
</x-jetstream.light-button>
|
||||
<x-jetstream.light-button wire:click="moveListItem({{ $index }}, 'options', {{ $i }}, 1)" :disabled="$loop->last" title="{{ __('Move down') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-down" />
|
||||
</x-jetstream.light-button>
|
||||
<x-jetstream.light-button wire:click="removeListItem({{ $index }}, 'options', {{ $i }})" title="{{ __('Remove') }}">
|
||||
<x-icon class="h-4 w-4" name="trash" />
|
||||
</x-jetstream.light-button>
|
||||
</div>
|
||||
@error("$prefix.options.$i.label")
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@endforeach
|
||||
</div>
|
||||
@error("$prefix.options")
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<x-jetstream.secondary-button class="mt-3" wire:click="addListItem({{ $index }}, 'options')">
|
||||
<x-icon class="mr-2 h-4 w-4" name="plus" />
|
||||
{{ __('Add option') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
{{-- List of free-text answers. Expects: $texts; optional $title --}}
|
||||
@if (!empty($texts))
|
||||
<div class="mt-4">
|
||||
@isset($title)
|
||||
<p class="text-sm text-theme-secondary mb-2">{{ $title }}</p>
|
||||
@endisset
|
||||
<ul class="max-h-64 overflow-y-auto space-y-2">
|
||||
@foreach ($texts as $text)
|
||||
<li class="p-3 bg-theme-surface rounded-md border border-theme-border text-sm text-theme-primary whitespace-pre-line">{{ $text }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,3 @@
|
||||
<p class="text-sm text-theme-secondary mb-3">
|
||||
{{ trans_choice(':count response|:count responses', $summary['total'], ['count' => $summary['total']]) }}
|
||||
</p>
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Proposed dates and times') }}</label>
|
||||
<div class="space-y-3">
|
||||
@foreach ($settings['slots'] ?? [] as $i => $slot)
|
||||
<div class="p-3 bg-theme-surface rounded-md border border-theme-border" wire:key="poll-q{{ $index }}-slot-{{ $slot['key'] }}">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-1">{{ __('Starts') }}</label>
|
||||
@include('polls.types._date', ['model' => "$prefix.slots.$i." . 'starts_at', 'key' => 'slot-' . $slot['key'] . '-starts_at'])
|
||||
@include('polls.types._error', ['field' => "$prefix.slots.$i.starts_at"])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-1">{{ __('Ends (optional)') }}</label>
|
||||
@include('polls.types._date', ['model' => "$prefix.slots.$i." . 'ends_at', 'key' => 'slot-' . $slot['key'] . '-ends_at'])
|
||||
@include('polls.types._error', ['field' => "$prefix.slots.$i.ends_at"])
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-end space-x-2">
|
||||
<x-jetstream.light-button wire:click="moveListItem({{ $index }}, 'slots', {{ $i }}, -1)" :disabled="$i === 0" title="{{ __('Move up') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-up" />
|
||||
</x-jetstream.light-button>
|
||||
<x-jetstream.light-button wire:click="moveListItem({{ $index }}, 'slots', {{ $i }}, 1)" :disabled="$loop->last" title="{{ __('Move down') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-down" />
|
||||
</x-jetstream.light-button>
|
||||
<x-jetstream.light-button wire:click="removeListItem({{ $index }}, 'slots', {{ $i }})" title="{{ __('Remove') }}">
|
||||
<x-icon class="h-4 w-4" name="trash" />
|
||||
</x-jetstream.light-button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@include('polls.types._error', ['field' => "$prefix.slots"])
|
||||
<x-jetstream.secondary-button class="mt-3" wire:click="addListItem({{ $index }}, 'slots')">
|
||||
<x-icon class="mr-2 h-4 w-4" name="plus" />
|
||||
{{ __('Add date and time') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
@include('polls.types._checkbox', ['model' => "$prefix.allow_if_needed", 'label' => __('Also offer "If needed"')])
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="space-y-3">
|
||||
@foreach ($type->slotLabels($settings, $locale) as $key => $label)
|
||||
<div class="p-3 bg-theme-surface rounded-md border border-theme-border" wire:key="{{ $prefix }}-slot-{{ $key }}">
|
||||
<p class="text-sm text-theme-primary mb-2">{{ $label }}</p>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
@foreach ($type->choices($settings) as $choice => $choiceLabel)
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="radio" wire:model="{{ $prefix }}.value.{{ $key }}" value="{{ $choice }}" name="{{ $prefix }}-{{ $key }}"
|
||||
class="border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50">
|
||||
<span class="text-sm text-theme-primary">{{ $choiceLabel }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
{{-- Per date only once some dates are answered; an empty question has its own message below --}}
|
||||
@unless ($errors->has("$prefix.value"))
|
||||
@include('polls.types._error', ['field' => "$prefix.value.$key"])
|
||||
@endunless
|
||||
</div>
|
||||
@endforeach
|
||||
@include('polls.types._error', ['field' => "$prefix.value"])
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
@include('polls.types._total')
|
||||
<div class="space-y-4">
|
||||
@foreach ($type->slotLabels($settings, $locale) as $key => $label)
|
||||
@php($counts = $summary['slots'][$key] ?? [])
|
||||
@php($isBest = in_array($key, $summary['best'], true))
|
||||
<div>
|
||||
@include('polls.types._bar', [
|
||||
'label' => $label . ($isBest ? ' – ' . __('best option') : ''),
|
||||
'count' => $counts['yes'] ?? 0,
|
||||
'total' => $summary['total'],
|
||||
'highlight' => $isBest,
|
||||
])
|
||||
<p class="mt-1 text-xs text-theme-secondary">
|
||||
{{ __('Yes') }}: {{ $counts['yes'] ?? 0 }}
|
||||
@if (!empty($settings['allow_if_needed']))
|
||||
· {{ __('If needed') }}: {{ $counts['if_needed'] ?? 0 }}
|
||||
@endif
|
||||
· {{ __('No') }}: {{ $counts['no'] ?? 0 }}
|
||||
</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="space-y-4">
|
||||
@include('polls.types._options-editor')
|
||||
@include('polls.types._checkbox', ['model' => "$prefix.allow_other", 'label' => __('Allow "Other" with a text field')])
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Minimum selections') }}</label>
|
||||
<x-input errorless wire:model="{{ $prefix }}.min_selections" type="number" min="1" placeholder="{{ __('No minimum') }}" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "$prefix.min_selections"])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Maximum selections') }}</label>
|
||||
<x-input errorless wire:model="{{ $prefix }}.max_selections" type="number" min="1" placeholder="{{ __('No maximum') }}" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "$prefix.max_selections"])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div x-data="{ selected: $wire.entangle('{{ $prefix }}.value') }" x-init="if (!Array.isArray(selected)) selected = []" class="space-y-2">
|
||||
@if (!empty($settings['min_selections']) || !empty($settings['max_selections']))
|
||||
<p class="text-sm text-theme-secondary">
|
||||
@if (!empty($settings['min_selections']) && !empty($settings['max_selections']))
|
||||
{{ __('Select :min to :max options.', ['min' => $settings['min_selections'], 'max' => $settings['max_selections']]) }}
|
||||
@elseif (!empty($settings['min_selections']))
|
||||
{{ trans_choice('Select at least :count option.|Select at least :count options.', $settings['min_selections'], ['count' => $settings['min_selections']]) }}
|
||||
@else
|
||||
{{ trans_choice('Select at most :count option.|Select at most :count options.', $settings['max_selections'], ['count' => $settings['max_selections']]) }}
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
@foreach ($type->optionLabels($settings, $locale, true) as $key => $label)
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="checkbox" x-model="selected" value="{{ $key }}"
|
||||
class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50">
|
||||
<span class="text-sm text-theme-primary">{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@include('polls.types._error', ['field' => "$prefix.value"])
|
||||
|
||||
@if (!empty($settings['allow_other']))
|
||||
<div x-show="Array.isArray(selected) && selected.includes('{{ \App\Polls\Types\MultiChoiceType::OTHER }}')" x-cloak class="pl-7">
|
||||
<x-input errorless wire:model="{{ $prefix }}.text" type="text" placeholder="{{ __('Other') }}..." class="w-full" maxlength="200" />
|
||||
@include('polls.types._error', ['field' => "$prefix.text"])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
@include('polls.types._total')
|
||||
<div class="space-y-3">
|
||||
@foreach ($type->optionLabels($settings, $locale, true) as $key => $label)
|
||||
@include('polls.types._bar', ['label' => $label, 'count' => $summary['counts'][$key] ?? 0, 'total' => $summary['total']])
|
||||
@endforeach
|
||||
</div>
|
||||
@include('polls.types._texts', ['texts' => $summary['other_texts'], 'title' => __('Other')])
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="space-y-4">
|
||||
@include('polls.types._options-editor')
|
||||
<div class="sm:w-1/2">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Number of options to rank') }}</label>
|
||||
<x-input errorless wire:model="{{ $prefix }}.top_n" type="number" min="1" placeholder="{{ __('All options') }}" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "$prefix.top_n"])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
{{--
|
||||
Click options in order of preference. Ranked options can be reordered by
|
||||
dragging or with the up/down buttons (keyboard and touch), and removed again.
|
||||
--}}
|
||||
@php($labels = $type->optionLabels($settings, $locale))
|
||||
@php($rankCount = $type->rankCount($settings))
|
||||
<div
|
||||
x-data="{
|
||||
ranked: $wire.entangle('{{ $prefix }}.value'),
|
||||
labels: @js($labels),
|
||||
rankCount: {{ $rankCount }},
|
||||
dragFrom: null,
|
||||
list() { return Array.isArray(this.ranked) ? this.ranked : []; },
|
||||
unranked() { return Object.keys(this.labels).filter(key => !this.list().includes(key)); },
|
||||
add(key) { if (this.list().length < this.rankCount) { this.ranked = [...this.list(), key]; } },
|
||||
remove(i) { const list = [...this.list()]; list.splice(i, 1); this.ranked = list; },
|
||||
move(from, to) {
|
||||
const list = [...this.list()];
|
||||
if (to < 0 || to >= list.length || from === to) { return; }
|
||||
list.splice(to, 0, list.splice(from, 1)[0]);
|
||||
this.ranked = list;
|
||||
},
|
||||
}"
|
||||
>
|
||||
<p class="text-sm text-theme-secondary mb-2">
|
||||
{{ trans_choice('Click your favourite first, then the next, until you have ranked :count option.|Click your favourite first, then the next, until you have ranked :count options.', $rankCount, ['count' => $rankCount]) }}
|
||||
</p>
|
||||
|
||||
<ol class="space-y-2">
|
||||
<template x-for="(key, i) in list()" :key="key">
|
||||
<li
|
||||
draggable="true"
|
||||
@dragstart="dragFrom = i"
|
||||
@dragover.prevent
|
||||
@drop.prevent="move(dragFrom, i); dragFrom = null"
|
||||
class="flex items-center space-x-2 p-2 bg-theme-surface rounded-md border border-theme-border cursor-move"
|
||||
>
|
||||
<span class="flex h-7 w-7 flex-none items-center justify-center rounded-full bg-theme-brand text-sm text-white" x-text="i + 1"></span>
|
||||
<span class="flex-1 text-sm text-theme-primary" x-text="labels[key]"></span>
|
||||
<button type="button" @click="move(i, i - 1)" :disabled="i === 0" class="p-1 text-theme-secondary disabled:opacity-30" title="{{ __('Move up') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-up" />
|
||||
</button>
|
||||
<button type="button" @click="move(i, i + 1)" :disabled="i === list().length - 1" class="p-1 text-theme-secondary disabled:opacity-30" title="{{ __('Move down') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-down" />
|
||||
</button>
|
||||
<button type="button" @click="remove(i)" class="p-1 text-theme-secondary" title="{{ __('Remove') }}">
|
||||
<x-icon class="h-4 w-4" name="x-mark" />
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</ol>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2" x-show="list().length < rankCount">
|
||||
<template x-for="key in unranked()" :key="key">
|
||||
<button type="button" @click="add(key)"
|
||||
class="rounded-md border border-theme-border bg-theme-background px-3 py-1 text-sm text-theme-primary hover:bg-theme-surface"
|
||||
x-text="labels[key]"></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@include('polls.types._error', ['field' => "$prefix.value"])
|
||||
@foreach (array_keys(array_values($labels)) as $i)
|
||||
@include('polls.types._error', ['field' => "$prefix.value.$i"])
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
@php($labels = $type->optionLabels($settings, $locale))
|
||||
@php($maxPoints = max(1, $summary['total'] * count($labels)))
|
||||
@include('polls.types._total')
|
||||
<div class="space-y-3">
|
||||
@foreach ($summary['points'] as $key => $points)
|
||||
@include('polls.types._bar', [
|
||||
'label' => ($loop->iteration) . '. ' . ($labels[$key] ?? $key),
|
||||
'count' => $points,
|
||||
'total' => $maxPoints,
|
||||
'highlight' => $loop->first && $points > 0,
|
||||
'suffix' => trans_choice(':count point|:count points', $points, ['count' => $points])
|
||||
. ' · ' . __('ranked first :count×', ['count' => $summary['first_place'][$key] ?? 0]),
|
||||
])
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-theme-secondary">
|
||||
{{ __('Points: with :count options, a first place earns :count points, a second place one less, and so on.', ['count' => count($labels)]) }}
|
||||
</p>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Lowest value') }}</label>
|
||||
<select wire:model="{{ $prefix }}.min" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
<option value="0">0</option>
|
||||
<option value="1">1</option>
|
||||
</select>
|
||||
@include('polls.types._error', ['field' => "$prefix.min"])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Highest value') }}</label>
|
||||
<select wire:model="{{ $prefix }}.max" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
@foreach (range(2, 10) as $max)
|
||||
<option value="{{ $max }}">{{ $max }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@include('polls.types._error', ['field' => "$prefix.max"])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Label for lowest value') }} ({{ strtoupper($locale) }})</label>
|
||||
<x-input wire:model="{{ $prefix }}.min_label.{{ $locale }}" type="text" placeholder="{{ __('e.g. Not at all') }}" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Label for highest value') }} ({{ strtoupper($locale) }})</label>
|
||||
<x-input wire:model="{{ $prefix }}.max_label.{{ $locale }}" type="text" placeholder="{{ __('e.g. Very much') }}" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
@php($minLabel = \App\Polls\Localized::pick($settings['min_label'] ?? [], $locale))
|
||||
@php($maxLabel = \App\Polls\Localized::pick($settings['max_label'] ?? [], $locale))
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-2" role="radiogroup">
|
||||
@foreach ($type->steps($settings) as $step)
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" wire:model="{{ $prefix }}.value" value="{{ $step }}" name="{{ $prefix }}" class="peer sr-only">
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-md border border-theme-border bg-theme-background text-sm text-theme-primary
|
||||
peer-checked:bg-theme-brand peer-checked:text-white peer-focus-visible:ring-2 peer-focus-visible:ring-theme-brand">
|
||||
{{ $step }}
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@if ($minLabel !== '' || $maxLabel !== '')
|
||||
<div class="mt-1 flex justify-between text-xs text-theme-secondary" style="max-width: {{ count($type->steps($settings)) * 3 }}rem">
|
||||
<span>{{ $minLabel }}</span>
|
||||
<span>{{ $maxLabel }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@include('polls.types._error', ['field' => "$prefix.value"])
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
@include('polls.types._total')
|
||||
@if ($summary['average'] !== null)
|
||||
<p class="text-sm text-theme-primary mb-3">
|
||||
{{ __('Average') }}: <strong>{{ number_format($summary['average'], 1) }}</strong>
|
||||
({{ $type->steps($settings)[0] }}–{{ last($type->steps($settings)) }})
|
||||
</p>
|
||||
@endif
|
||||
<div class="space-y-3">
|
||||
@foreach ($summary['distribution'] as $step => $count)
|
||||
@include('polls.types._bar', ['label' => (string) $step, 'count' => $count, 'total' => $summary['total']])
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="space-y-4">
|
||||
@include('polls.types._options-editor')
|
||||
@include('polls.types._checkbox', ['model' => "$prefix.allow_other", 'label' => __('Allow "Other" with a text field')])
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div x-data="{ choice: $wire.entangle('{{ $prefix }}.value') }" class="space-y-2">
|
||||
@foreach ($type->optionLabels($settings, $locale, true) as $key => $label)
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="radio" x-model="choice" value="{{ $key }}" name="{{ $prefix }}"
|
||||
class="border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50">
|
||||
<span class="text-sm text-theme-primary">{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@include('polls.types._error', ['field' => "$prefix.value"])
|
||||
|
||||
@if (!empty($settings['allow_other']))
|
||||
<div x-show="choice === '{{ \App\Polls\Types\SingleChoiceType::OTHER }}'" x-cloak class="pl-7">
|
||||
<x-input errorless wire:model="{{ $prefix }}.text" type="text" placeholder="{{ __('Other') }}..." class="w-full" maxlength="200" />
|
||||
@include('polls.types._error', ['field' => "$prefix.text"])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
@include('polls.types._total')
|
||||
<div class="space-y-3">
|
||||
@foreach ($type->optionLabels($settings, $locale, true) as $key => $label)
|
||||
@include('polls.types._bar', ['label' => $label, 'count' => $summary['counts'][$key] ?? 0, 'total' => $summary['total']])
|
||||
@endforeach
|
||||
</div>
|
||||
@include('polls.types._texts', ['texts' => $summary['other_texts'], 'title' => __('Other')])
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="space-y-4">
|
||||
@include('polls.types._checkbox', ['model' => "$prefix.multiline", 'label' => __('Allow a long answer (several lines)')])
|
||||
<div class="sm:w-1/2">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Maximum number of characters') }}</label>
|
||||
<x-input errorless wire:model="{{ $prefix }}.max_length" type="number" min="10" max="5000" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "$prefix.max_length"])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
@php($maxLength = (int) ($settings['max_length'] ?? 1000))
|
||||
<div>
|
||||
@if (!empty($settings['multiline']))
|
||||
<x-textarea errorless wire:model="{{ $prefix }}.text" rows="4" maxlength="{{ $maxLength }}" class="w-full" />
|
||||
@else
|
||||
<x-input errorless wire:model="{{ $prefix }}.text" type="text" maxlength="{{ $maxLength }}" class="w-full" />
|
||||
@endif
|
||||
@include('polls.types._error', ['field' => "$prefix.text"])
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
@include('polls.types._total')
|
||||
@include('polls.types._texts', ['texts' => $summary['texts']])
|
||||
@@ -0,0 +1 @@
|
||||
<p class="text-sm text-theme-secondary">{{ __('This question type has no settings.') }}</p>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">
|
||||
{{ __('Label for "Maybe, if..."') }} ({{ strtoupper($locale) }})
|
||||
</label>
|
||||
<x-input errorless wire:model="{{ $prefix }}.conditional_label.{{ $locale }}" type="text" placeholder="{{ __('Maybe, if...') }}" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "$prefix.conditional_label.$locale"])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div x-data="{ choice: $wire.entangle('{{ $prefix }}.value') }" class="space-y-2">
|
||||
@foreach ($type->choices($settings, $locale) as $key => $label)
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="radio" x-model="choice" value="{{ $key }}" name="{{ $prefix }}"
|
||||
class="border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50">
|
||||
<span class="text-sm text-theme-primary">{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@include('polls.types._error', ['field' => "$prefix.value"])
|
||||
|
||||
<div x-show="choice === '{{ \App\Polls\Types\YesNoConditionalType::MAYBE_IF }}'" x-cloak class="pl-7">
|
||||
<x-input errorless wire:model="{{ $prefix }}.text" type="text" placeholder="{{ __('Under which condition?') }}" class="w-full" maxlength="500" />
|
||||
@include('polls.types._error', ['field' => "$prefix.text"])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
@include('polls.types._total')
|
||||
<div class="space-y-3">
|
||||
@foreach ($type->choices($settings, $locale) as $key => $label)
|
||||
@include('polls.types._bar', ['label' => $label, 'count' => $summary['counts'][$key] ?? 0, 'total' => $summary['total']])
|
||||
@endforeach
|
||||
</div>
|
||||
@include('polls.types._texts', ['texts' => $summary['conditions'], 'title' => __('Conditions')])
|
||||
@@ -0,0 +1,214 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
{{ __('Verify poll') }}
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-theme-background shadow-xl sm:rounded-lg p-6 sm:px-20 space-y-6">
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl lg:text-3xl font-semibold text-theme-primary">{{ $title }}</h2>
|
||||
@if ($slug)
|
||||
<a href="{{ route('poll.show', ['slug' => $slug]) }}" class="text-sm text-theme-secondary underline">{{ __('Back to the poll') }}</a>
|
||||
@endif
|
||||
<p class="mt-4 text-sm text-theme-secondary">
|
||||
{{ __('Every vote is recorded as an anonymous ballot in a ledger. Each ballot contains the fingerprint of the one before it, so changing, adding or removing a ballot afterwards breaks the chain. You can check your own vote with your receipt code, and recount all ballots yourself.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- State of the ledger -->
|
||||
<div class="p-4 bg-theme-surface rounded-md border border-theme-border space-y-2">
|
||||
<p class="text-sm text-theme-primary">
|
||||
<strong>{{ __('Ballot ledger') }}:</strong>
|
||||
@if ($chain['valid'])
|
||||
<span class="text-green-700">{{ __('intact') }}</span> ({{ trans_choice(':count entry|:count entries', $chain['entries'], ['count' => $chain['entries']]) }})
|
||||
@else
|
||||
<span class="text-red-600">{{ __('broken at entry :sequence', ['sequence' => $chain['broken_at']]) }}</span>
|
||||
@endif
|
||||
</p>
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ __('Ballots counted: :active. Cast: :cast, changed: :change, withdrawn: :withdraw.', $counts) }}
|
||||
</p>
|
||||
@if ($poll->sealed_at)
|
||||
<p class="text-sm text-theme-primary">
|
||||
<strong>{{ __('Sealed') }}:</strong> {{ $poll->sealed_at->locale(app()->getLocale())->isoFormat('LLL') }}
|
||||
<code class="block mt-1 break-all font-mono text-xs text-theme-secondary">{{ $poll->seal_hash }}</code>
|
||||
</p>
|
||||
<p class="text-xs text-theme-secondary">{{ __('Note this seal somewhere: if the ledger is ever changed afterwards, it will no longer match.') }}</p>
|
||||
@else
|
||||
<p class="text-sm text-theme-secondary">{{ __('The ledger is sealed when the poll closes.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Receipt check -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-theme-primary mb-3">{{ __('Check your vote') }}</h3>
|
||||
<livewire:polls.receipt-check :pollId="$poll->id" />
|
||||
</div>
|
||||
|
||||
@if ($ballotsHidden)
|
||||
<p class="text-sm text-theme-secondary">{{ __('The ballot list and the recount become available here when the results of this poll are visible to you.') }}</p>
|
||||
@endif
|
||||
|
||||
@if ($public)
|
||||
<!-- Recount -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-theme-primary mb-3">{{ __('Recount') }}</h3>
|
||||
<p class="text-sm {{ $recount['matches'] ? 'text-green-700' : 'text-red-600' }}">
|
||||
{{ $recount['matches'] ? __('The recount of the ballots matches the published result.') : __('The recount of the ballots does not match the published result.') }}
|
||||
</p>
|
||||
<ul class="mt-2 space-y-1">
|
||||
@foreach ($recount['questions'] as $questionId => $matches)
|
||||
<li class="text-sm text-theme-primary">
|
||||
<span class="{{ $matches ? 'text-green-700' : 'text-red-600' }}">{{ $matches ? '✓' : '✗' }}</span>
|
||||
{{ $questions->get($questionId)?->labelFor(app()->getLocale()) }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Browser verification -->
|
||||
<div x-data="{
|
||||
running: false,
|
||||
result: null,
|
||||
error: null,
|
||||
labels: @js(['chain' => __('Hash chain'), 'recount' => __('Recount matches the published result'), 'seal' => __('Seal')]),
|
||||
async run() {
|
||||
this.running = true;
|
||||
this.error = null;
|
||||
this.result = null;
|
||||
try {
|
||||
const response = await fetch(@js(route('polls.ledger', $poll)), { headers: { Accept: 'application/json' } });
|
||||
this.result = await window.PollLedgerVerifier.verify(await response.json());
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
this.running = false;
|
||||
},
|
||||
}">
|
||||
<h3 class="text-lg font-semibold text-theme-primary mb-3">{{ __('Verify in your browser') }}</h3>
|
||||
<p class="text-sm text-theme-secondary mb-3">
|
||||
{{ __('Your browser downloads all ballots and recalculates every fingerprint and the count itself, without relying on the server. The code that does this is included in this page.') }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<x-jetstream.button type="button" x-on:click="run()" x-bind:disabled="running" class="bg-theme-brand hover:bg-opacity-80">
|
||||
<span x-show="!running">{{ __('Verify in your browser') }}</span>
|
||||
<span x-show="running" x-cloak>{{ __('Processing...') }}</span>
|
||||
</x-jetstream.button>
|
||||
<a href="{{ route('polls.ledger', $poll) }}" class="text-sm text-theme-secondary underline" download>{{ __('Download the ledger (JSON)') }}</a>
|
||||
</div>
|
||||
<template x-if="result">
|
||||
<div class="mt-4 p-4 rounded-md border" :class="result.valid ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'">
|
||||
<p class="text-sm font-semibold" :class="result.valid ? 'text-green-800' : 'text-red-800'"
|
||||
x-text="result.valid ? @js(__('Everything checks out.')) : @js(__('The ledger did not pass the check.'))"></p>
|
||||
<ul class="mt-2 space-y-1">
|
||||
<template x-for="item in result.checks" :key="item.key">
|
||||
<li class="text-sm" :class="item.ok ? 'text-green-800' : 'text-red-800'">
|
||||
<span x-text="item.ok ? '✓' : '✗'"></span>
|
||||
<span x-text="labels[item.key] || item.key"></span>
|
||||
<span class="text-xs" x-text="item.detail"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="error" x-cloak class="mt-3 text-sm text-red-600" x-text="error"></p>
|
||||
</div>
|
||||
|
||||
<!-- Anonymous ballot list -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-theme-primary mb-3">{{ __('All ballots') }}</h3>
|
||||
<p class="text-sm text-theme-secondary mb-3">{{ __('Ballots are shown by fingerprint, never by voter. Free text is hidden as [text]; it is still part of the fingerprint.') }}</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">#</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Type') }}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Fingerprint') }}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Status') }}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Answers') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach ($ballots as $entry)
|
||||
@php($replaced = $replacedBy->get($entry->sequence))
|
||||
<tr>
|
||||
<td class="px-3 py-2 text-gray-500">{{ $entry->sequence }}</td>
|
||||
<td class="px-3 py-2 text-gray-900">{{ __(['cast' => 'Vote', 'change' => 'Changed vote', 'withdraw' => 'Withdrawal', 'seal' => 'Seal'][$entry->entry_type]) }}</td>
|
||||
<td class="px-3 py-2 font-mono text-xs text-gray-500">{{ $entry->fingerprint ? substr($entry->fingerprint, 0, 12) : '-' }}</td>
|
||||
<td class="px-3 py-2 text-gray-500">
|
||||
@if (!$entry->isBallot())
|
||||
-
|
||||
@elseif ($replaced === 'withdraw')
|
||||
{{ __('Withdrawn') }}
|
||||
@elseif ($replaced)
|
||||
{{ __('Replaced') }}
|
||||
@else
|
||||
{{ __('Counted') }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-900">
|
||||
@if ($entry->isBallot())
|
||||
@foreach ($entry->payload['answers'] ?? [] as $answer)
|
||||
@php($question = $questions->get($answer['question_id']))
|
||||
@if ($question && app(\App\Polls\QuestionTypeRegistry::class)->has($question->type))
|
||||
<div>
|
||||
<span class="text-gray-500">{{ \Illuminate\Support\Str::limit($question->labelFor(app()->getLocale()), 40) }}:</span>
|
||||
{{ $question->questionType()->answerToText($answer['value'] ?? null, !empty($answer['text_sha256']) ? '[' . __('text') . ']' : null, $question->settings ?? [], app()->getLocale()) ?: '-' }}
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@elseif ($entry->entry_type === 'withdraw')
|
||||
{{ __('Ballot #:sequence no longer counts', ['sequence' => $entry->refers_to_sequence]) }}
|
||||
@elseif ($entry->entry_type === 'seal')
|
||||
{{ __(':count ballots counted at closing', ['count' => $entry->payload['active_ballots'] ?? 0]) }}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $ballots->links() }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Limits -->
|
||||
<div class="p-4 bg-theme-surface rounded-md border border-theme-border">
|
||||
<h3 class="text-sm font-semibold text-theme-primary">{{ __('What this can and cannot prove') }}</h3>
|
||||
|
||||
<p class="mt-3 text-sm font-medium text-theme-primary">{{ __('This page can show:') }}</p>
|
||||
<ul class="mt-1 list-disc pl-5 space-y-1 text-sm text-theme-secondary">
|
||||
<li>{{ __('That your vote was counted the way you cast it. Enter your receipt code to see your ballot.') }}</li>
|
||||
<li>{{ __('That no vote was changed, added or removed afterwards. Each ballot carries a fingerprint of the ballot before it, like links in a chain.') }}</li>
|
||||
<li>{{ __('That nothing changed after the poll was sealed. Write down the seal at the top: if it is still the same later, nothing has changed.') }}</li>
|
||||
</ul>
|
||||
<p class="mt-2 text-sm text-theme-secondary italic">
|
||||
{{ __('Example: if someone changed one vote from Yes to No, its fingerprint would no longer match the next ballot. The chain breaks and the check shows it.') }}
|
||||
</p>
|
||||
|
||||
<p class="mt-3 text-sm font-medium text-theme-primary">{{ __('This page cannot show:') }}</p>
|
||||
<ul class="mt-1 list-disc pl-5 space-y-1 text-sm text-theme-secondary">
|
||||
<li>{{ __('Who cast the other ballots. The list is anonymous. The platform does keep track of who voted, but only so that each profile votes once.') }}</li>
|
||||
</ul>
|
||||
|
||||
<p class="mt-3 text-sm font-medium text-theme-primary">{{ __('Good to know:') }}</p>
|
||||
<ul class="mt-1 list-disc pl-5 space-y-1 text-sm text-theme-secondary">
|
||||
<li>{{ __('Keep your receipt code to yourself: anyone who has it can see how you voted.') }}</li>
|
||||
<li>{{ __('We cannot send your code again if you lose it, because we only store a fingerprint of it.') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($public)
|
||||
@push('scripts')
|
||||
<script>
|
||||
{!! file_get_contents(resource_path('js/poll-ledger-verifier.js')) !!}
|
||||
</script>
|
||||
@endpush
|
||||
@endif
|
||||
</x-app-layout>
|
||||
Reference in new issue
Block a user