voting system with nostr added

This commit is contained in:
fsociety
2024-09-29 01:02:04 +02:00
parent a0ef037b2d
commit 354680f702
43 changed files with 3017 additions and 20688 deletions

View File

@@ -1,3 +1,6 @@
@import 'utility-patterns.css';
@import 'flatpickr.css';
@tailwind base;
@tailwind components;
@tailwind utilities;

237
resources/css/flatpickr.css Normal file
View File

@@ -0,0 +1,237 @@
/* Customise flatpickr */
* {
--calendarPadding: 24px;
--daySize: 36px;
--daysWidth: calc(var(--daySize)*7);
}
@keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -8px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.flatpickr-calendar {
border: inherit;
@apply bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700/60 left-1/2;
margin-left: calc(calc(var(--daysWidth) + calc(var(--calendarPadding)*2))*0.5*-1);
padding: var(--calendarPadding);
width: calc(var(--daysWidth) + calc(var(--calendarPadding)*2));
}
@screen lg {
.flatpickr-calendar {
@apply left-0 right-auto;
margin-left: 0;
}
}
.flatpickr-right.flatpickr-calendar {
@apply right-0 left-auto;
margin-left: 0;
}
.flatpickr-calendar.animate.open {
animation: fpFadeInDown 200ms ease-out;
}
.flatpickr-calendar.static {
position: absolute;
top: calc(100% + 4px);
}
.flatpickr-calendar.static.open {
z-index: 20;
}
.flatpickr-days {
width: var(--daysWidth);
}
.dayContainer {
width: var(--daysWidth);
min-width: var(--daysWidth);
max-width: var(--daysWidth);
}
.flatpickr-day {
@apply bg-gray-50 dark:bg-gray-700/20 text-sm font-medium text-gray-600 dark:text-gray-100;
max-width: var(--daySize);
height: var(--daySize);
line-height: var(--daySize);
}
.flatpickr-day,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay {
border: none;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay,
.flatpickr-day.notAllowed,
.flatpickr-day.notAllowed.prevMonthDay,
.flatpickr-day.notAllowed.nextMonthDay {
@apply bg-transparent;
}
.flatpickr-day,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay,
.flatpickr-day.selected.startRange,
.flatpickr-day.startRange.startRange,
.flatpickr-day.endRange.startRange,
.flatpickr-day.selected.endRange,
.flatpickr-day.startRange.endRange,
.flatpickr-day.endRange.endRange,
.flatpickr-day.selected.startRange.endRange,
.flatpickr-day.startRange.startRange.endRange,
.flatpickr-day.endRange.startRange.endRange {
border-radius: 0;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay,
.flatpickr-day.notAllowed,
.flatpickr-day.notAllowed.prevMonthDay,
.flatpickr-day.notAllowed.nextMonthDay {
@apply text-gray-400 dark:text-gray-500;
}
.rangeMode .flatpickr-day {
margin: 0;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected.inRange,
.flatpickr-day.startRange.inRange,
.flatpickr-day.endRange.inRange,
.flatpickr-day.selected:focus,
.flatpickr-day.startRange:focus,
.flatpickr-day.endRange:focus,
.flatpickr-day.selected:hover,
.flatpickr-day.startRange:hover,
.flatpickr-day.endRange:hover,
.flatpickr-day.selected.prevMonthDay,
.flatpickr-day.startRange.prevMonthDay,
.flatpickr-day.endRange.prevMonthDay,
.flatpickr-day.selected.nextMonthDay,
.flatpickr-day.startRange.nextMonthDay,
.flatpickr-day.endRange.nextMonthDay {
@apply bg-violet-600 text-violet-50;
}
.flatpickr-day.inRange,
.flatpickr-day.prevMonthDay.inRange,
.flatpickr-day.nextMonthDay.inRange,
.flatpickr-day.today.inRange,
.flatpickr-day.prevMonthDay.today.inRange,
.flatpickr-day.nextMonthDay.today.inRange,
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover,
.flatpickr-day:focus,
.flatpickr-day.prevMonthDay:focus,
.flatpickr-day.nextMonthDay:focus,
.flatpickr-day.today:hover,
.flatpickr-day.today:focus {
@apply bg-violet-500 text-violet-50;
}
.flatpickr-day.inRange,
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)),
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)),
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) {
box-shadow: none;
}
.flatpickr-months {
align-items: center;
margin-top: -8px;
margin-bottom: 6px;
}
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
position: static;
height: auto;
@apply text-gray-400 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-300;
}
.flatpickr-months .flatpickr-prev-month svg,
.flatpickr-months .flatpickr-next-month svg {
width: 7px;
height: 11px;
fill: currentColor;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
@apply fill-current;
}
.flatpickr-months .flatpickr-prev-month {
margin-left: -10px;
}
.flatpickr-months .flatpickr-next-month {
margin-right: -10px;
}
.flatpickr-months .flatpickr-month {
@apply text-gray-800 dark:text-gray-100;
height: auto;
line-height: inherit;
}
.flatpickr-current-month {
@apply text-sm font-medium;
position: static;
height: auto;
width: auto;
left: auto;
padding: 0;
}
.flatpickr-current-month span.cur-month {
@apply font-medium m-0;
}
.flatpickr-current-month span.cur-month:hover {
background: none;
}
.flatpickr-current-month input.cur-year {
font-weight: inherit;
box-shadow: none !important;
}
.numInputWrapper:hover {
background: none;
}
.numInputWrapper span {
display: none;
}
span.flatpickr-weekday {
@apply text-gray-400 dark:text-gray-500 font-medium text-xs;
}
.flatpickr-calendar.arrowTop::before,
.flatpickr-calendar.arrowTop::after,
.flatpickr-calendar.arrowBottom::before,
.flatpickr-calendar.arrowBottom::after {
display: none;
}

View File

@@ -0,0 +1,138 @@
/* Typography */
.h1 {
@apply text-4xl font-extrabold tracking-tighter;
}
.h2 {
@apply text-3xl font-extrabold tracking-tighter;
}
.h3 {
@apply text-3xl font-extrabold;
}
.h4 {
@apply text-2xl font-extrabold tracking-tight;
}
@screen md {
.h1 {
@apply text-5xl;
}
.h2 {
@apply text-4xl;
}
}
/* Buttons */
.btn,
.btn-lg,
.btn-sm,
.btn-xs {
@apply font-medium text-sm inline-flex items-center justify-center border border-transparent rounded-lg leading-5 shadow-sm transition;
}
.btn {
@apply px-3 py-2;
}
.btn-lg {
@apply px-4 py-3;
}
.btn-sm {
@apply px-2 py-1;
}
.btn-xs {
@apply px-2 py-0.5;
}
/* Forms */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
-webkit-appearance: none;
}
.form-input,
.form-textarea,
.form-multiselect,
.form-select,
.form-checkbox,
.form-radio {
@apply bg-white dark:bg-gray-900/30 border focus:ring-0 focus:ring-offset-0 dark:disabled:bg-gray-700/30 dark:disabled:border-gray-700 dark:disabled:hover:border-gray-700;
}
.form-checkbox {
@apply rounded;
}
.form-input,
.form-textarea,
.form-multiselect,
.form-select {
@apply text-sm text-gray-800 dark:text-gray-100 leading-5 py-2 px-3 border-gray-200 hover:border-gray-300 focus:border-gray-300 dark:border-gray-700/60 dark:hover:border-gray-600 dark:focus:border-gray-600 shadow-sm rounded-lg;
}
.form-input,
.form-textarea {
@apply placeholder-gray-400 dark:placeholder-gray-500;
}
.form-select {
@apply pr-10;
}
.form-checkbox,
.form-radio {
@apply text-violet-500 checked:bg-violet-500 dark:checked:border-transparent border border-gray-300 focus:border-violet-300 dark:border-gray-700/60 dark:focus:border-violet-500/50;
}
/* Switch element */
.form-switch {
@apply relative select-none;
width: 44px;
}
.form-switch label {
@apply block overflow-hidden cursor-pointer h-6 rounded-full;
}
.form-switch label > span:first-child {
@apply absolute block rounded-full;
width: 20px;
height: 20px;
top: 2px;
left: 2px;
right: 50%;
transition: all .15s ease-out;
}
.form-switch input[type="checkbox"]:checked + label {
@apply bg-violet-500;
}
.form-switch input[type="checkbox"]:checked + label > span:first-child {
left: 22px;
}
.form-switch input[type="checkbox"]:disabled + label {
@apply cursor-not-allowed bg-gray-100 dark:bg-gray-700/20 border border-gray-200 dark:border-gray-700/60;
}
.form-switch input[type="checkbox"]:disabled + label > span:first-child {
@apply bg-gray-400 dark:bg-gray-600;
}
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View File

@@ -1,3 +1,8 @@
import {Alpine, Livewire} from '../../vendor/livewire/livewire/dist/livewire.esm';
import nostrApp from "./nostrApp.js";
import nostrLogin from "./nostrLogin.js";
import './bootstrap';
// Light switcher
@@ -6,3 +11,12 @@ document.documentElement.classList.add('dark');
document.querySelector('html').style.colorScheme = 'dark';
localStorage.setItem('dark-mode', true);
document.dispatchEvent(new CustomEvent('darkMode', { detail: { mode: 'on' } }));
Alpine.store('nostr', {
user: null,
});
Alpine.data('nostrApp', nostrApp);
Alpine.data('nostrLogin', nostrLogin);
Livewire.start();

15
resources/js/nostrApp.js Normal file
View File

@@ -0,0 +1,15 @@
export default (livewireComponent) => ({
signThisEvent: livewireComponent.entangle('signThisEvent'),
init() {
// on change of signThisEvent, call the method
this.$watch('signThisEvent', async () => {
const toBeSigned = JSON.parse(this.signThisEvent);
console.log(toBeSigned);
const signedEvent = await window.nostr.signEvent(toBeSigned);
this.$wire.call('signEvent', signedEvent);
});
},
});

View File

@@ -0,0 +1,29 @@
export default () => ({
openNostrLogin() {
window.nostr.getPublicKey();
},
init() {
// listen for nostr auth events
document.addEventListener('nlAuth', (e) => {
// type is login, signup or logout
if (e.detail.type === 'login' || e.detail.type === 'signup') {
console.log('User logged in');
// fetch profile from /api/nostr/profile/{publicKey}
fetch('/api/nostr/profile/' + e.detail.pubkey)
.then(response => response.json())
.then(data => {
console.log('Profile fetched', data);
// store the profile in AlpineJS store
Alpine.store('nostr', { user: data });
this.$dispatch('nostrLoggedIn', {pubkey: data.pubkey});
});
} else {
console.log('User logged out')
Alpine.store('nostr', { user: null });
}
})
},
});

View File

@@ -5,17 +5,19 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? 'Page Title' }}</title>
@livewireStyles
@vite(['resources/js/app.js','resources/css/app.css'])
@googlefonts
<script src="https://kit.fontawesome.com/866fd3d0ab.js" crossorigin="anonymous"></script>
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js' data-perms="sign_event:1,sign_event:0"
data-theme="default" data-dark-mode="true"></script>
@wireUiScripts
@stack('scripts')
</head>
<body
class="font-sans antialiased bg-gray-100 dark:bg-[#222222] text-gray-600 dark:text-gray-400"
:class="{ 'sidebar-expanded': sidebarExpanded }"
x-data="{ sidebarOpen: false, sidebarExpanded: localStorage.getItem('sidebar-expanded') == 'true' }"
x-data="{ sidebarOpen: false, sidebarExpanded: localStorage.getItem('sidebar-expanded') == 'true', inboxSidebarOpen: false }"
x-init="$watch('sidebarExpanded', value => localStorage.setItem('sidebar-expanded', value))"
>
<script>
@@ -25,9 +27,11 @@
document.querySelector('body').classList.remove('sidebar-expanded');
}
</script>
<div class="flex h-[100dvh] overflow-hidden">
@include('components.layouts.sidebar')
<div class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
<div x-data="nostrLogin"
class="flex h-[100dvh] overflow-hidden">
<livewire:layout.sidebar/>
<div
class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
<!-- Site header -->
<header
class="sticky top-0 before:absolute before:inset-0 before:backdrop-blur-md before:bg-white/90 dark:before:bg-[#222222]/90 lg:before:bg-[#222222]/90 dark:lg:before:bg-[#222222]/90 before:-z-10 max-lg:shadow-sm z-30">
@@ -125,11 +129,10 @@
</div>
</header>
<main class="grow">
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
{{ $slot }}
</div>
{{ $slot }}
</main>
</div>
</div>
@livewireScriptConfig
</body>
</html>

View File

@@ -0,0 +1,34 @@
<!-- Association group -->
<div>
<h3 class="text-xs uppercase text-gray-400 dark:text-gray-500 font-semibold pl-3">
<span class="hidden lg:block lg:sidebar-expanded:hidden 2xl:hidden text-center w-6"
aria-hidden="true">•••</span>
<span class="lg:hidden lg:sidebar-expanded:block 2xl:block">Verein</span>
</h3>
<ul class="mt-3">
<li class="{{ $currentRoute === 'association.profile' ? $isCurrentRouteClass : $isNotCurrentRouteClass }}">
<a class="block text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:text-white truncate transition" href="{{ route('association.profile') }}">
<div class="flex items-center">
<i class="fa-sharp-duotone fa-solid fa-id-card-clip h-6 w-6"></i>
<span class="text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">Meine Mitgliedschaft</span>
</div>
</a>
</li>
<li class="{{ $currentRoute === 'association.election' ? $isCurrentRouteClass : $isNotCurrentRouteClass }}">
<a class="block text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:text-white truncate transition" href="{{ route('association.election', ['election' => date('Y')]) }}">
<div class="flex items-center">
<i class="fa-sharp-duotone fa-solid fa-check-to-slot h-6 w-6"></i>
<span class="text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">Vorstand</span>
</div>
</a>
</li>
<li class="{{ $currentRoute === 'association.elections' ? $isCurrentRouteClass : $isNotCurrentRouteClass }}">
<a class="block text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:text-white truncate transition" href="{{ route('association.elections') }}">
<div class="flex items-center">
<i class="fa-sharp-duotone fa-solid fa-booth-curtain h-6 w-6"></i>
<span class="text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">Wahlen</span>
</div>
</a>
</li>
</ul>
</div>

View File

@@ -15,7 +15,7 @@
</a>
</li>
<li class="pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0">
<a class="block text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:text-white truncate transition" href="{{ route('meetups.table') }}">
<a class="block text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:text-white truncate transition" href="{{ route('meetups.grid') }}">
<div class="flex items-center">
<i class="fa-sharp-duotone fa-solid fa-handshake-angle h-6 w-6"></i>
<span class="text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">Alle Meetups</span>

View File

@@ -1,4 +1,11 @@
<div class="min-w-fit">
@php
$isCurrentRouteClass = 'pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0 bg-[linear-gradient(135deg,var(--tw-gradient-stops))] from-violet-500/[0.12] dark:from-violet-500/[0.24] to-violet-500/[0.04]';
$isNotCurrentRouteClass = 'pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0';
$isCurrentSubItem = 'block text-violet-500 transition truncate';
$isNotCurrentSubItem = 'block text-gray-500/90 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition truncate';
@endphp
<!-- Sidebar backdrop (mobile only) -->
<div
class="fixed inset-0 bg-gray-900 bg-opacity-30 z-40 lg:hidden lg:z-auto transition-opacity duration-200"
@@ -10,7 +17,7 @@
<!-- Sidebar -->
<div
id="sidebar"
class="flex flex-col absolute z-40 left-0 top-0 lg:static lg:left-auto lg:top-auto lg:translate-x-0 h-[100dvh] overflow-y-scroll lg:overflow-y-auto no-scrollbar w-64 lg:w-20 lg:sidebar-expanded:!w-64 2xl:!w-64 shrink-0 bg-white dark:bg-[#1B1B1B] shadow-sm rounded-r-2xl p-4 transition-all duration-200 ease-in-out"
class="flex flex-col absolute z-40 left-0 top-0 lg:static lg:left-auto lg:top-auto lg:translate-x-0 h-[100dvh] overflow-y-scroll lg:overflow-y-auto no-scrollbar w-64 lg:w-20 lg:sidebar-expanded:!w-64 2xl:!w-64 shrink-0 bg-white dark:bg-[#222222] shadow-sm rounded-r-2xl p-4 transition-all duration-200 ease-in-out"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-64'"
@click.outside="sidebarOpen = false"
@keydown.escape.window="sidebarOpen = false"
@@ -38,7 +45,8 @@
<!-- Links -->
<div class="space-y-8">
@include('components.layouts.navigation.meetups')
@include('components.layouts.navigation.meetups', ['isCurrentRouteClass' => $isCurrentRouteClass, 'isNotCurrentRouteClass' => $isNotCurrentRouteClass])
@include('components.layouts.navigation.association', ['isCurrentRouteClass' => $isCurrentRouteClass, 'isNotCurrentRouteClass' => $isNotCurrentRouteClass])
{{--@include('components.layouts.navigation.events')
@include('components.layouts.navigation.courses')
@include('components.layouts.navigation.nostr')

View File

@@ -0,0 +1,87 @@
<?php
use function Livewire\Volt\state;
use function Livewire\Volt\mount;
state(['currentRoute' => '']);
mount(function() {
$currentLivewireRouteName = request()->route()->getName();
$this->currentRoute = $currentLivewireRouteName;
});
?>
<div class="min-w-fit">
@php
$isCurrentRouteClass = 'pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0 bg-[linear-gradient(135deg,var(--tw-gradient-stops))] from-orange-500/[0.12] dark:from-orange-500/[0.24] to-orange-500/[0.04]';
$isNotCurrentRouteClass = 'pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0';
$isCurrentSubItem = 'block text-orange-500 transition truncate';
$isNotCurrentSubItem = 'block text-gray-500/90 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition truncate';
@endphp
<!-- Sidebar backdrop (mobile only) -->
<div
class="fixed inset-0 bg-gray-900 bg-opacity-30 z-40 lg:hidden lg:z-auto transition-opacity duration-200"
:class="sidebarOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'"
aria-hidden="true"
x-cloak
></div>
<!-- Sidebar -->
<div
id="sidebar"
class="flex flex-col absolute z-40 left-0 top-0 lg:static lg:left-auto lg:top-auto lg:translate-x-0 h-[100dvh] overflow-y-scroll lg:overflow-y-auto no-scrollbar w-64 lg:w-20 lg:sidebar-expanded:!w-64 2xl:!w-64 shrink-0 bg-white dark:bg-[#222222] shadow-sm rounded-r-2xl p-4 transition-all duration-200 ease-in-out"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-64'"
@click.outside="sidebarOpen = false"
@keydown.escape.window="sidebarOpen = false"
x-cloak="lg"
>
<!-- Sidebar header -->
<div class="flex justify-between mb-10 pr-3 sm:px-2">
<!-- Close button -->
<button class="lg:hidden text-gray-500 hover:text-gray-400" @click.stop="sidebarOpen = !sidebarOpen"
aria-controls="sidebar" :aria-expanded="sidebarOpen">
<span class="sr-only">Close sidebar</span>
<svg class="w-6 h-6 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M10.7 18.7l1.4-1.4L7.8 13H20v-2H7.8l4.3-4.3-1.4-1.4L4 12z"/>
</svg>
</button>
<!-- Logo -->
<img src="{{ asset('img/einundzwanzig-horizontal-inverted.svg') }}" alt="Logo" width="auto" height="32">
</div>
@php
$activeLinkGroupClass = ' bg-[linear-gradient(135deg,var(--tw-gradient-stops))] from-amber-500/[0.12] dark:from-amber-500/[0.24] to-amber-500/[0.04]';
$activeItemClass = 'block text-amber-500 transition truncate';
@endphp
<!-- Links -->
<div class="space-y-8">
@include('components.layouts.navigation.meetups')
@include('components.layouts.navigation.association')
{{--@include('components.layouts.navigation.events')
@include('components.layouts.navigation.courses')
@include('components.layouts.navigation.nostr')
@include('components.layouts.navigation.legacy')--}}
</div>
<!-- Expand / collapse button -->
<div class="pt-3 hidden lg:inline-flex 2xl:hidden justify-end mt-auto">
<div class="w-12 pl-4 pr-3 py-2">
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors"
@click="sidebarExpanded = !sidebarExpanded">
<span class="sr-only">Expand / collapse sidebar</span>
<svg class="shrink-0 fill-current text-gray-400 dark:text-gray-500 sidebar-expanded:rotate-180"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path
d="M15 16a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v14a1 1 0 0 1-1 1ZM8.586 7H1a1 1 0 1 0 0 2h7.586l-2.793 2.793a1 1 0 1 0 1.414 1.414l4.5-4.5A.997.997 0 0 0 12 8.01M11.924 7.617a.997.997 0 0 0-.217-.324l-4.5-4.5a1 1 0 0 0-1.414 1.414L8.586 7M12 7.99a.996.996 0 0 0-.076-.373Z"/>
</svg>
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,502 @@
<?php
use Livewire\Volt\Component;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Key\Key;
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');
state(['currentPubkey' => null]);
state(['events' => []]);
state(['election' => fn() => $election]);
state(['plebs' => []]);
state(['search' => '']);
state(['signThisEvent' => '']);
mount(function () {
$this->plebs = \App\Models\EinundzwanzigPleb::query()
->with([
'profile',
])
->whereIn('association_status', [3, 4])
->orderBy('association_status', 'desc')
->get()
->toArray();
$this->loadEvents();
});
on([
'nostrLoggedIn' => function ($pubkey) {
$this->currentPubkey = $pubkey;
},
]);
on(['echo:votes,.newVote' => function () {
$this->loadEvents();
}]);
updated([
'search' => function ($value) {
$this->plebs = \App\Models\EinundzwanzigPleb::query()
->with([
'profile',
])
->where('pubkey', 'like', "%$value%")
->orWhereHas('profile', function ($query) use ($value) {
$query->where('name', 'ilike', "%$value%");
})
->orderBy('association_status', 'desc')
->get()
->toArray();
},
]);
$loadEvents = function () {
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter1 = new Filter();
$filter1->setKinds([2121]); // You can add multiple kind numbers
$filters = [$filter1]; // You can add multiple filters.
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relays = [
new Relay('ws://relay:7000'),
];
$relaySet = new RelaySet();
$relaySet->setRelays($relays);
$request = new Request($relaySet, $requestMessage);
$response = $request->send();
$this->events = collect($response['ws://relay:7000'])
->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) {
$note = new NostrEvent();
$note->setContent($pubkey . ',' . $type);
$note->setKind(2121);
$this->signThisEvent = $note->toJson();
};
$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);
$relayUrl = 'ws://relay:7000';
$relay = new Relay($relayUrl);
$relay->setMessage($eventMessage);
$result = $relay->send();
Broadcast::on('votes')
->as('newVote')
->sendNow();
};
?>
<x-layouts.app title="{{ __('Wahl') }}">
@volt
<div class="relative flex h-full" x-data="nostrApp(@this)">
@php
$positions = [
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
'vice_president' => ['icon' => 'fa-user-group-crown', 'title' => 'Vizepräsidium'],
'finances' => ['icon' => 'fa-bitcoin-sign', 'title' => 'Finanzen'],
'secretary' => ['icon' => 'fa-stapler', 'title' => 'Sekretär (Akurat)'],
'press_officer' => ['icon' => 'fa-newspaper', 'title' => 'Pressewart'],
'it_manager' => ['icon' => 'fa-server', 'title' => 'Technikwart'],
];
$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(function ($event) {
return $event['pubkey'] . $event['type'];
})
->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/' . $pleb['pubkey'] }}"
onerror="this.onerror=null; this.src='https://robohash.org/{{ $pleb['pubkey'] }}';"
width="32"
height="32"
alt="{{ $pleb['pubkey'] }}"/>
<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,
];
});
@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 justify-between items-center">
<x-badge success label="Die Wahl ist geöffnet bis zum 31.12.2024 um 22:00 Uhr"/>
</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 sm:justify-between mb-4">
<h1 class="text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
Wahl des Vereinsvorstands
</h1>
<button
class="text-xs inline-flex font-medium bg-sky-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">
<div class="grid grid-cols-12 gap-6">
@foreach($positions as $type => $position)
@if($electionConfig->firstWhere('type', $type))
<div
class="col-span-full sm:col-span-6 xl:col-span-4 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 {{ $position['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">{{ $position['title'] }}</h2>
</div>
<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'] }}" gewählt</span>
@else
<span>Klicke auf den Kandidaten, den du wählen möchtest.</span>
@endif
</div>
</div>
<footer class="mt-5">
<div class="flex justify-between items-center">
@foreach($electionConfig->firstWhere('type', $type)['candidates'] as $c)
<div wire:click="vote('{{ $c['pubkey'] }}', '{{ $type }}')"
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>
@endif
@endforeach
</div>
</div>
</div>
<!-- Log events -->
<div class="mt-6">
<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">Logged Votes on Nostr <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>
</div>
@endif
</div>
@endvolt
</x-layouts.app>

View File

@@ -0,0 +1,54 @@
<?php
use Livewire\Volt\Component;
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.elections');
state(['elections' => []]);
mount(function () {
$this->elections = \App\Models\Election::query()
->get()
->toArray();
});
updated([
]);
$saveElection = function ($index) {
$election = $this->elections[$index];
$electionModel = \App\Models\Election::find($election['id']);
$electionModel->candidates = $election['candidates'];
$electionModel->save();
};
?>
<x-layouts.app title="{{ __('Wahlen') }}">
@volt
<div class="relative flex h-full">
@foreach($elections as $election)
<div class="w-1/3 p-4">
<div class="shadow-lg rounded-lg overflow-hidden">
{{ $election['year'] }}
</div>
<div class="shadow-lg rounded-lg overflow-hidden">
<x-textarea wire:model="elections.{{ $loop->index }}.candidates" rows="25" label="candidates" placeholder="" />
</div>
<div class="py-2">
<x-button label="Speichern" wire:click="saveElection({{ $loop->index }})"/>
</div>
</div>
@endforeach
</div>
@endvolt
</x-layouts.app>

View File

@@ -0,0 +1,164 @@
<?php
use Livewire\Volt\Component;
use function Livewire\Volt\computed;
use function Livewire\Volt\mount;
use function Livewire\Volt\state;
use function Livewire\Volt\with;
use function Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{on};
name('association.profile');
?>
<x-layouts.app title="{{ __('Wahl') }}">
@volt
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
<!-- Page header -->
<div class="mb-8">
<!-- Title -->
<h1 class="text-2xl md:text-3xl text-[#1B1B1B] dark:text-gray-100 font-bold">
Einundzwanzig ist, was du draus machst
</h1>
</div>
<div class="bg-white dark:bg-[#1B1B1B] shadow-sm rounded-xl mb-8">
<div class="flex flex-col md:flex-row md:-mr-px">
<!-- Sidebar -->
<div
class="flex flex-nowrap overflow-x-scroll no-scrollbar md:block md:overflow-auto px-3 py-6 border-b md:border-b-0 md:border-r border-gray-200 dark:border-gray-700/60 min-w-60 md:space-y-3">
<!-- Group 1 -->
<div>
<div class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase mb-3">
Meine Mitgliedschaft
</div>
<ul class="flex flex-nowrap md:block mr-3 md:mr-0">
<li class="mr-0.5 md:mr-0 md:mb-0.5">
<a class="flex items-center px-2.5 py-2 rounded-lg whitespace-nowrap bg-[linear-gradient(135deg,var(--tw-gradient-stops))] from-orange-500/[0.12] dark:from-orange-500/[0.24] to-orange-500/[0.04]"
href="settings.html">
<i class="fa-sharp-duotone fa-solid fa-id-card-clip shrink-0 fill-current text-orange-400 mr-2"></i>
<span
class="text-sm font-medium text-orange-500 dark:text-orange-400">Status</span>
</a>
</li>
{{--<li class="mr-0.5 md:mr-0 md:mb-0.5">
<a class="flex items-center px-2.5 py-2 rounded-lg whitespace-nowrap"
href="notifications.html">
<svg class="shrink-0 fill-current text-gray-400 dark:text-gray-500 mr-2" width="16"
height="16" viewBox="0 0 16 16">
<path
d="m9 12.614 4.806 1.374a.15.15 0 0 0 .174-.21L8.133 2.082a.15.15 0 0 0-.268 0L2.02 13.777a.149.149 0 0 0 .174.21L7 12.614V9a1 1 0 1 1 2 0v3.614Zm-1 1.794-5.257 1.503c-1.798.514-3.35-1.355-2.513-3.028L6.076 1.188c.791-1.584 3.052-1.584 3.845 0l5.848 11.695c.836 1.672-.714 3.54-2.512 3.028L8 14.408Z"/>
</svg>
<span
class="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200">My Notifications</span>
</a>
</li>--}}
</ul>
</div>
<!-- Group 2 -->
{{--<div>
<div class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase mb-3">Experience
</div>
<ul class="flex flex-nowrap md:block mr-3 md:mr-0">
<li class="mr-0.5 md:mr-0 md:mb-0.5">
<a class="flex items-center px-2.5 py-2 rounded-lg whitespace-nowrap"
href="feedback.html">
<svg class="shrink-0 fill-current text-gray-400 dark:text-gray-500 mr-2" width="16"
height="16" viewBox="0 0 16 16">
<path
d="M14.3.3c.4-.4 1-.4 1.4 0 .4.4.4 1 0 1.4l-8 8c-.2.2-.4.3-.7.3-.3 0-.5-.1-.7-.3-.4-.4-.4-1 0-1.4l8-8zM15 7c.6 0 1 .4 1 1 0 4.4-3.6 8-8 8s-8-3.6-8-8 3.6-8 8-8c.6 0 1 .4 1 1s-.4 1-1 1C4.7 2 2 4.7 2 8s2.7 6 6 6 6-2.7 6-6c0-.6.4-1 1-1z"/>
</svg>
<span
class="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200">Give Feedback</span>
</a>
</li>
</ul>
</div>--}}
</div>
<!-- Panel -->
<div class="grow">
<!-- Panel body -->
<div class="p-6 space-y-6">
<h2 class="text-2xl text-[#1B1B1B] dark:text-gray-100 font-bold mb-5">Aktueller Status</h2>
<!-- Picture -->
<section>
<div class="flex items-center">
<x-button label="Mit Nostr verbinden" @click="openNostrLogin" x-show="!$store.nostr.user"/>
<template x-if="$store.nostr.user">
<div class="flex items">
<img class="w-12 h-12 rounded-full"
x-bind:src="$store.nostr.user.picture"
alt="">
<div class="ml-4">
<h3 class="text-lg leading-snug text-[#1B1B1B] dark:text-gray-100 font-bold" x-text="$store.nostr.user.nip05"></h3>
<div class="text-sm text-gray-500 dark:text-gray-400" x-text="$store.nostr.user.nip05"></div>
</div>
</div>
</template>
</div>
</section>
<!-- Business Profile -->
<section>
<h3 class="text-xl leading-snug text-[#1B1B1B] dark:text-gray-100 font-bold mb-1">
passives Mitglied werden
</h3>
<div class="text-sm">
TEXT
</div>
<div class="sm:flex sm:items-center space-y-4 sm:space-y-0 sm:space-x-4 mt-5">
<div class="sm:w-1/3">
<x-button label="Beantragen"/>
</div>
</div>
</section>
<!-- Email -->
<section>
<h3 class="text-xl leading-snug text-[#1B1B1B] dark:text-gray-100 font-bold mb-1">aktives
Mitglied werden</h3>
<div class="text-sm">
TEXT
</div>
<div class="sm:flex sm:items-center space-y-4 sm:space-y-0 sm:space-x-4 mt-5">
<div class="sm:w-1/3">
<x-button label="Beantragen"/>
</div>
</div>
</section>
</div>
<!-- Panel footer -->
{{--<footer>
<div class="flex flex-col px-6 py-5 border-t border-gray-200 dark:border-gray-700/60">
<div class="flex self-end">
<button
class="btn dark:bg-[#1B1B1B] border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 text-[#1B1B1B] dark:text-gray-300">
Cancel
</button>
<button
class="btn bg-gray-900 text-gray-100 hover:bg-[#1B1B1B] dark:bg-gray-100 dark:text-[#1B1B1B] dark:hover:bg-white ml-3">
Save Changes
</button>
</div>
</div>
</footer>--}}
</div>
</div>
</div>
</div>
@endvolt
</x-layouts.app>

View File

@@ -0,0 +1,247 @@
<?php
use Livewire\Volt\Component;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Key\Key;
use swentel\nostr\Message\RequestMessage;
use swentel\nostr\Relay\Relay;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;
use function Livewire\Volt\computed;
use function Livewire\Volt\mount;
use function Livewire\Volt\state;
use function Livewire\Volt\with;
use function Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{on};
name('meetups.grid');
?>
<x-layouts.app title="{{ __('Meetups') }}">
@volt
<div class="relative flex">
<!-- Profile sidebar -->
<div
id="profile-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="profileSidebarOpen ? '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)]">
<!-- Profile 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">
<!-- Profile image -->
<div class="relative">
<div class="grow flex items-center truncate">
<div class="truncate">
<span
class="font-semibold text-gray-800 dark:text-gray-100">All meetups</span>
</div>
</div>
</div>
<!-- Add button -->
<button
class="p-1.5 shrink-0 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 shadow-sm ml-2">
<svg class="fill-current text-violet-500" width="16" height="16"
viewBox="0 0 16 16">
<path
d="M15 7H9V1c0-.6-.4-1-1-1S7 .4 7 1v6H1c-.6 0-1 .4-1 1s.4 1 1 1h6v6c0 .6.4 1 1 1s1-.4 1-1V9h6c.6 0 1-.4 1-1s-.4-1-1-1Z"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Group body -->
<div class="px-5 py-4">
<!-- Search form -->
<div class="relative">
<label for="profile-search" class="sr-only">Search</label>
<input id="profile-search" class="form-input w-full pl-9 bg-white dark:bg-gray-800 rounded"
type="search" placeholder="Search…"/>
<button class="absolute inset-0 right-auto group cursor-default" 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>
</div>
<!-- Team members -->
<div class="mt-4">
<div class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase mb-3">
Countries
</div>
<ul class="mb-6">
<li class="-mx-2">
<button
class="w-full p-2 rounded-lg bg-[linear-gradient(135deg,var(--tw-gradient-stops))] from-violet-500/[0.12] dark:from-violet-500/[0.24] to-violet-500/[0.04]"
@click="profileSidebarOpen = false">
<div class="flex items-center">
<div class="relative mr-2">
<img class="w-8 h-8 rounded-full"
src="{{ asset('vendor/blade-country-flags/1x1-de.svg') }}"
width="32" height="32" alt="User 08"/>
</div>
<div class="truncate">
<span class="text-sm font-medium text-gray-800 dark:text-gray-100">Deutschland</span>
</div>
</div>
</button>
</li>
<li class="-mx-2">
<button
class="w-full p-2"
@click="profileSidebarOpen = false">
<div class="flex items-center">
<div class="relative mr-2">
<img class="w-8 h-8 rounded-full"
src="{{ asset('vendor/blade-country-flags/1x1-at.svg') }}"
width="32" height="32" alt="User 08"/>
</div>
<div class="truncate">
<span class="text-sm font-medium text-gray-800 dark:text-gray-100">Österreich</span>
</div>
</div>
</button>
</li>
<li class="-mx-2">
<button
class="w-full p-2"
@click="profileSidebarOpen = false">
<div class="flex items-center">
<div class="relative mr-2">
<img class="w-8 h-8 rounded-full"
src="{{ asset('vendor/blade-country-flags/1x1-ch.svg') }}"
width="32" height="32" alt="User 08"/>
</div>
<div class="truncate">
<span class="text-sm font-medium text-gray-800 dark:text-gray-100">Schweiz</span>
</div>
</div>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Profile body -->
<div
class="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out"
:class="profileSidebarOpen ? 'translate-x-1/3' : 'translate-x-0'"
>
<!-- Profile background -->
<div class="relative h-56 bg-gray-200 dark:bg-gray-900">
<img class="object-cover object-top h-full w-full" src="{{ asset('img/meetup_saarland.jpg') }}"
width="979" height="220"
alt="Profile background"/>
<!-- Close button -->
<button
class="md:hidden absolute top-4 left-4 sm:left-6 text-white opacity-80 hover:opacity-100"
@click.stop="profileSidebarOpen = !profileSidebarOpen"
aria-controls="profile-sidebar"
:aria-expanded="profileSidebarOpen"
>
<span class="sr-only">Close sidebar</span>
<svg class="w-6 h-6 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M10.7 18.7l1.4-1.4L7.8 13H20v-2H7.8l4.3-4.3-1.4-1.4L4 12z"/>
</svg>
</button>
</div>
<!-- Content -->
<div class="relative px-4 sm:px-6 pb-8">
<!-- Pre-header -->
<div class="-mt-16 mb-6 sm:mb-3">
<div class="flex flex-col items-center sm:flex-row sm:justify-between sm:items-end">
<!-- Avatar -->
<div class="inline-flex -ml-1 -mt-1 mb-4 sm:mb-0" style="height: 128px">
</div>
<!-- Actions -->
<div class="flex space-x-2 sm:mb-2">
{{-- ACTIONS --}}
</div>
</div>
</div>
<div class="grid xl:grid-cols-2 gap-6 mb-8">
<!-- Item 1 -->
<article class="flex bg-white dark:bg-[#1B1B1B] shadow-sm rounded-xl overflow-hidden">
<!-- Image -->
<a class="relative block w-24 sm:w-56 xl:sidebar-expanded:w-40 2xl:sidebar-expanded:w-56 shrink-0" href="meetups-post.html">
<img class="absolute object-cover object-center w-full h-full" src="./images/meetups-thumb-01.jpg" width="220" height="236" alt="Meetup 01" />
<!-- Like button -->
<button class="absolute top-0 right-0 mt-4 mr-4">
<div class="text-gray-100 bg-gray-900 bg-opacity-60 rounded-full">
<span class="sr-only">Like</span>
<svg class="h-8 w-8 fill-current" viewBox="0 0 32 32">
<path d="M22.682 11.318A4.485 4.485 0 0019.5 10a4.377 4.377 0 00-3.5 1.707A4.383 4.383 0 0012.5 10a4.5 4.5 0 00-3.182 7.682L16 24l6.682-6.318a4.5 4.5 0 000-6.364zm-1.4 4.933L16 21.247l-5.285-5A2.5 2.5 0 0112.5 12c1.437 0 2.312.681 3.5 2.625C17.187 12.681 18.062 12 19.5 12a2.5 2.5 0 011.785 4.251h-.003z" />
</svg>
</div>
</button>
</a>
<!-- Content -->
<div class="grow p-5 flex flex-col">
<div class="grow">
<div class="text-sm font-semibold text-violet-500 uppercase mb-2">Mon 27 Dec, 2024</div>
<a class="inline-flex mb-2" href="meetups-post.html">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100">Silicon Valley Bootstrapper Breakfast Online for 2024</h3>
</a>
<div class="text-sm">Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts.</div>
</div>
<!-- Footer -->
<div class="flex justify-between items-center mt-3">
<!-- Tag -->
<div class="text-xs inline-flex items-center font-medium border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-400 rounded-full text-center px-2.5 py-1">
<svg class="w-4 h-3 fill-gray-400 dark:fill-gray-500 mr-2" viewBox="0 0 16 12">
<path d="m16 2-4 2.4V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7.6l4 2.4V2ZM2 10V2h8v8H2Z" />
</svg>
<span>Online Event</span>
</div>
<!-- Avatars -->
<div class="flex items-center space-x-2">
<div class="flex -space-x-3 -ml-0.5">
<img class="rounded-full border-2 border-white dark:border-gray-800 box-content" src="./images/avatar-01.jpg" width="28" height="28" alt="User 01" />
<img class="rounded-full border-2 border-white dark:border-gray-800 box-content" src="./images/avatar-04.jpg" width="28" height="28" alt="User 04" />
<img class="rounded-full border-2 border-white dark:border-gray-800 box-content" src="./images/avatar-05.jpg" width="28" height="28" alt="User 05" />
</div>
<div class="text-xs font-medium text-gray-400 dark:text-gray-500 italic">+22</div>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
</div>
@endvolt
</x-layouts.app>

View File

@@ -0,0 +1,135 @@
<?php
use Livewire\Volt\Component;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Key\Key;
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 Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{on};
name('meetups.mockup');
state(['events' => []]);
state(['title' => '']);
state(['description' => '']);
state(['signThisEvent' => '']);
mount(function () {
$this->loadEvents();
});
$loadEvents = function() {
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter1 = new Filter();
$filter1->setKinds([31924]); // You can add multiple kind numbers
$filter1->setLimit(25); // Limit to fetch only a maximum of 25 events
$filters = [$filter1]; // You can add multiple filters.
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relays = [
new Relay('ws://nostream:8008'),
];
$relaySet = new RelaySet();
$relaySet->setRelays($relays);
$request = new Request($relaySet, $requestMessage);
$response = $request->send();
$this->events = collect($response['ws://nostream:8008'])
->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();
};
$save = function () {
$note = new NostrEvent();
$note->setContent($this->description);
$note->setKind(31924);
$note->setTags([
['d', str()->uuid()->toString()],
['title', $this->title],
]);
$this->signThisEvent = $note->toJson();
};
$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);
$relayUrl = 'ws://nostream:8008';
$relay = new Relay($relayUrl);
$relay->setMessage($eventMessage);
$result = $relay->send();
$this->title = '';
$this->description = '';
$this->loadEvents();
};
?>
<x-layouts.app title="{{ __('Mockup') }}">
@volt
<div class="relative" x-data="nostrApp(@this)">
<div class="flex items-center space-x-2 mt-12">
<div>
<x-input wire:model.live.debounce="title" label="Title"/>
</div>
<div>
<x-textarea wire:model.live.debounce="description" label="Description"/>
</div>
<div>
<x-button wire:click="save" label="Save"/>
</div>
</div>
<h1 class="text-2x font-bold py-6">Meetups</h1>
<ul class="border-t border-white space-y-4 divide-y divide-white">
@foreach($events as $event)
<li>
<div class="flex items">
<div class="flex items-center space-x-2">
<div>
Name: {{ collect($event['tags'])->firstWhere(0, 'title')[1] }}
</div>
<div>
Beschreibung: {{ $event['content'] }}
</div>
<div>
@dump($event)
</div>
</div>
</div>
</li>
@endforeach
</ul>
</div>
@endvolt
</x-layouts.app>