🚀 feat(project-support): add project proposal form and listing pages with image uploads and voting functionality

This commit is contained in:
fsociety
2024-10-23 18:10:14 +02:00
parent 85cccd1c11
commit c6b3593341
21 changed files with 693 additions and 21 deletions

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ProjectProposalForm extends Form
{
#[Validate('required|min:5')]
public $name = '';
#[Validate('required|numeric|min:21')]
public $support_in_sats = '';
#[Validate('required|string|min:5')]
public $description = '';
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Cookie;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class ProjectProposal extends Model implements HasMedia
{
use InteractsWithMedia;
use HasSlug;
/**
* The attributes that aren't mass assignable.
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
* @var array
*/
protected $casts = [
'id' => 'integer',
'einundzwanzig_pleb_id' => 'integer',
];
protected static function booted()
{
}
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['name'])
->saveSlugsTo('slug')
->usingLanguage(Cookie::get('lang', config('app.locale')));
}
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Fit::Crop, 300, 300)
->nonQueued();
$this
->addMediaConversion('thumb')
->fit(Fit::Crop, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this
->addMediaCollection('main')
->singleFile()
->useFallbackUrl(asset('img/einundzwanzig.png'));
}
public function einundzwanzigPleb(): BelongsTo
{
return $this->belongsTo(EinundzwanzigPleb::class);
}
public function votes(): HasMany
{
return $this->hasMany(Vote::class);
}
}

39
app/Models/Vote.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Vote extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'einundzwanzig_pleb_id' => 'integer',
'project_proposal_id' => 'integer',
'value' => 'bool',
];
public function einundzwanzigPleb(): BelongsTo
{
return $this->belongsTo(EinundzwanzigPleb::class);
}
public function projectProposal(): BelongsTo
{
return $this->belongsTo(ProjectProposal::class);
}
}

16
composer.lock generated
View File

@@ -6413,16 +6413,16 @@
},
{
"name": "spatie/laravel-medialibrary",
"version": "11.9.1",
"version": "11.9.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-medialibrary.git",
"reference": "ff589ea5532a33d84faeb64bfdfd59057b4148b8"
"reference": "6a39eca52236bc1e1261f366d4022996521ae843"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/ff589ea5532a33d84faeb64bfdfd59057b4148b8",
"reference": "ff589ea5532a33d84faeb64bfdfd59057b4148b8",
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/6a39eca52236bc1e1261f366d4022996521ae843",
"reference": "6a39eca52236bc1e1261f366d4022996521ae843",
"shasum": ""
},
"require": {
@@ -6506,7 +6506,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-medialibrary/issues",
"source": "https://github.com/spatie/laravel-medialibrary/tree/11.9.1"
"source": "https://github.com/spatie/laravel-medialibrary/tree/11.9.2"
},
"funding": [
{
@@ -6518,7 +6518,7 @@
"type": "github"
}
],
"time": "2024-09-02T06:32:15+00:00"
"time": "2024-10-18T14:24:58+00:00"
},
{
"name": "spatie/laravel-package-tools",
@@ -12769,12 +12769,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": {},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::create('project_proposals', function (Blueprint $table) {
$table->id();
$table->foreignId('einundzwanzig_pleb_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->string('slug')->unique();
$table->string('name')->unique();
$table->unsignedBigInteger('support_in_sats');
$table->text('description');
$table->timestamps();
});
Schema::enableForeignKeyConstraints();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_proposals');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->foreignId('einundzwanzig_pleb_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->foreignId('project_proposal_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->unsignedInteger('value');
$table->text('reason')->nullable();
$table->timestamps();
});
Schema::enableForeignKeyConstraints();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('votes');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->morphs('model');
$table->uuid()->nullable()->unique();
$table->string('collection_name');
$table->string('name');
$table->string('file_name');
$table->string('mime_type')->nullable();
$table->string('disk');
$table->string('conversions_disk')->nullable();
$table->unsignedBigInteger('size');
$table->json('manipulations');
$table->json('custom_properties');
$table->json('generated_conversions');
$table->json('responsive_images');
$table->unsignedInteger('order_column')->nullable()->index();
$table->nullableTimestamps();
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('project_proposals', function (Blueprint $table) {
$table->boolean('accepted')->default(false);
$table->unsignedInteger('sats_paid')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('project_proposals', function (Blueprint $table) {
//
});
}
};

1
public/site.webmanifest Normal file
View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -1,5 +1,6 @@
import {Alpine, Livewire} from '../../vendor/livewire/livewire/dist/livewire.esm';
import nostrDefault from "./nostrDefault.js";
import nostrApp from "./nostrApp.js";
import nostrLogin from "./nostrLogin.js";
import nostrZap from "./nostrZap.js";
@@ -18,6 +19,7 @@ Alpine.store('nostr', {
user: null,
});
Alpine.data('nostrDefault', nostrDefault);
Alpine.data('nostrApp', nostrApp);
Alpine.data('nostrLogin', nostrLogin);
Alpine.data('nostrZap', nostrZap);

View File

@@ -0,0 +1,5 @@
export default (livewireComponent) => ({
isAllowed: livewireComponent.entangle('isAllowed', true),
});

View File

@@ -0,0 +1,30 @@
<div
wire:ignore
x-data
x-init="
FilePond.registerPlugin(
FilePondPluginImagePreview,
FilePondPluginImageExifOrientation,
FilePondPluginFileValidateSize,
FilePondPluginImageEdit
);
FilePond.setOptions({
labelIdle: '{{ 'Drag & Drop Deiner Dateien oder <span class="filepond--label-action"> in Ordner suchen </span>' }}',
allowMultiple: {{ isset($attributes['multiple']) ? 'true' : 'false' }},
server: {
process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
@this.upload('{{ $attributes['wire:model'] }}', file, load, error, progress)
},
revert: (filename, load) => {
@this.removeUpload('{{ $attributes['wire:model'] }}', filename, load)
},
load: (source, load, error, progress, abort, headers) => {
@this.load('{{ $attributes['wire:model'] }}', load, error, progress, abort, headers)
},
},
});
FilePond.create($refs.input);
"
>
<input type="file" x-ref="input" name="{{ $attributes['name'] }}">
</div>

View File

@@ -0,0 +1,13 @@
@props([
'for',
'label',
])
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-700 sm:pt-5">
<label for="{{ $for }}"
class="block text-sm font-medium text-gray-100 sm:mt-px sm:pt-2">
{{ $label }}
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
{{ $slot }}
</div>
</div>

View File

@@ -0,0 +1,65 @@
@props(['model'])
<div
wire:ignore
x-data="{
value: $wire.entangle('{{ $model }}'),
init() {
let editor = new EasyMDE({
element: this.$refs.editor,
lineNumbers: true,
uploadImage: false,
spellChecker: false,
{{-- imageMaxSize: 1024 * 1024 * 10,--}}
{{-- imageUploadFunction: (file, onSuccess, onError) => {--}}
{{-- @this.upload('images', file, (uploadedFilename) => {--}}
{{-- const currentImage = @this.get('currentImage');--}}
{{-- const temporaryUrls = @this.get('temporaryUrls');--}}
{{-- onSuccess(temporaryUrls[currentImage]);--}}
{{-- @this.set('currentImage', currentImage + 1)--}}
{{-- }, () => {--}}
{{-- // Error callback.--}}
{{-- }, (event) => {--}}
{{-- // Progress callback.--}}
{{-- // event.detail.progress contains a number between 1 and 100 as the upload progresses.--}}
{{-- })--}}
{{-- },--}}
showIcons: [
'heading',
'heading-smaller',
'heading-bigger',
'heading-1',
'heading-2',
'heading-3',
'code',
'table',
'quote',
'strikethrough',
'unordered-list',
'ordered-list',
'clean-block',
'horizontal-rule',
'undo',
'redo',
//'upload-image',
],
})
editor.value(this.value)
editor.codemirror.on('change', () => {
this.value = editor.value()
})
},
}"
class="w-full"
>
<div class="prose max-w-none">
<textarea x-ref="editor"></textarea>
</div>
<style>
.EasyMDEContainer {
background-color: white;
}
</style>
</div>

View File

@@ -11,6 +11,8 @@
@vite(['resources/js/app.js','resources/css/app.css'])
@googlefonts
<script src="https://kit.fontawesome.com/866fd3d0ab.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
</head>
<body
class="font-sans antialiased bg-gray-100 dark:bg-[#222222] text-gray-600 dark:text-gray-400"

View File

@@ -22,6 +22,14 @@
</div>
</a>
</li>
<li class="{{ $currentRoute === 'association.projectSupport' ? $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.projectSupport') }}">
<div class="flex items-center">
<i class="fa-sharp-duotone fa-solid fa-hand-heart h-4 w-4"></i>
<span class="text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">Projekt-Unterstützungen</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">

View File

@@ -47,14 +47,6 @@ mount(function () {
}
});
on([
'nostrLoggedOut' => function () {
$this->isAllowed = false;
$this->currentPubkey = null;
$this->currentPleb = null;
},
]);
on([
'nostrLoggedIn' => function ($pubkey) {
$this->currentPubkey = $pubkey;
@@ -74,7 +66,12 @@ on([
'echo:votes,.newVote' => function () {
$this->loadEvents();
$this->loadBoardEvents();
}
},
'nostrLoggedOut' => function () {
$this->isAllowed = false;
$this->currentPubkey = null;
$this->currentPleb = null;
},
]);
updated([

View File

@@ -0,0 +1,29 @@
<?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 Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{state, mount, on, computed};
name('association.projectSupport.form');
state([
'projectProposal' => fn() => $projectProposal,
]);
?>
<x-layouts.app title="Welcome">
@volt
<div>
@dd($projectProposal)
</div>
@endvolt
</x-layouts.app>

View File

@@ -0,0 +1,116 @@
<?php
use App\Livewire\Forms\ProjectProposalForm;
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 Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{state, mount, on, computed, form, usesFileUploads};
name('association.projectSupport.create');
form(ProjectProposalForm::class);
state([
'image',
'isAllowed' => false,
'currentPubkey' => null,
'currentPleb' => null,
]);
usesFileUploads();
on([
'nostrLoggedIn' => function ($pubkey) {
$this->currentPubkey = $pubkey;
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
if ($this->currentPleb->association_status->value < 3) {
return $this->js('alert("Du bist hierzu berechtigt.")');
}
$this->isAllowed = true;
},
'nostrLoggedOut' => function () {
$this->isAllowed = false;
$this->currentPubkey = null;
$this->currentPleb = null;
},
]);
$save = function () {
$this->form->validate();
\App\Models\ProjectProposal::query()->create([
...$this->form->all(),
'einundzwanzig_pleb_id' => $this->currentPleb->id,
]);
return redirect()->route('association.projectSupport');
};
?>
<x-layouts.app title="Welcome">
@volt
<div x-cloak x-show="isAllowed" class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="nostrDefault(@this)">
<form class="space-y-8 divide-y divide-gray-700 pb-24">
<div class="space-y-8 divide-y divide-gray-700 sm:space-y-5">
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
<x-input.group :for=" md5('image')" :label="__('Bild')">
<div class="py-4">
@if ($image && str($image->getMimeType())->contains(['image/jpeg','image/jpg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp']))
<div class="text-gray-200">{{ __('Preview') }}:</div>
<img class="h-48 object-contain" src="{{ $image->temporaryUrl() }}">
@endif
@if (isset($projectProposal) && $projectProposal->getFirstMediaUrl('main'))
<div class="text-gray-200">{{ __('Current picture') }}:</div>
<img class="h-48 object-contain" src="{{ $projectProposal->getFirstMediaUrl('main') }}">
@endif
</div>
<input class="text-gray-200" type="file" wire:model="image">
@error('image') <span class="text-red-500">{{ $message }}</span> @enderror
</x-input.group>
<x-input.group :for="md5('form.name')" :label="__('Name')">
<x-input autocomplete="off" wire:model.debounce="form.name"
:placeholder="__('Name')"/>
</x-input.group>
<x-input.group :for="md5('form.name')" :label="__('Beabsichtigte Unterstützung in Sats')">
<x-input type="number" autocomplete="off" wire:model.debounce="form.support_in_sats"
:placeholder="__('Beabsichtigte Unterstützung in Sats')"/>
</x-input.group>
<x-input.group :for="md5('form.description')">
<x-slot name="label">
<div>
{{ __('Beschreibung') }}
</div>
<div
class="text-amber-500 text-xs py-2">{{ __('Bitte verfasse einen ausführlichen und verständlichen Antragstext, damit die Abstimmung über eine mögliche Förderung erfolgen kann.') }}</div>
</x-slot>
<div
class="text-amber-500 text-xs py-2">{{ __('Für Bilder in Markdown verwende bitte z.B. Imgur oder einen anderen Anbieter.') }}</div>
<x-input.simple-mde model="form.description"/>
@error('form.description') <span
class="text-red-500 py-2">{{ $message }}</span> @enderror
</x-input.group>
<x-input.group :for="md5('save')" label="">
<x-button primary wire:click="save">
<i class="fa fa-thin fa-save"></i>
{{ __('Save') }}
</x-button>
</x-input.group>
</div>
</div>
</form>
</div>
@endvolt
</x-layouts.app>

View File

@@ -0,0 +1,141 @@
<?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 Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{state, mount, on, computed};
name('association.projectSupport');
state([
'search' => '',
'projects' => fn()
=> \App\Models\ProjectProposal::query()
->with([
'einundzwanzigPleb.profile',
'votes',
])
->get(),
]);
?>
<x-layouts.app title="Projekt Unterstützungen">
@volt
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
<!-- Page header -->
<div class="sm:flex sm:justify-between sm:items-center mb-5">
<!-- Left: Title -->
<div class="mb-4 sm:mb-0">
<h1 class="text-2xl md:text-3xl text-gray-800 dark:text-gray-100 font-bold">
Einundzwanzig Projektunterstützungen
</h1>
</div>
<!-- Right: Actions -->
<div class="grid grid-flow-col sm:auto-cols-max justify-start sm:justify-end gap-2">
<!-- Search form -->
<form class="relative">
<x-input type="search" wire:model.live.debounce="search"
placeholder="Suche…"/>
</form>
<!-- Add meetup button -->
<x-button :href="route('association.projectSupport.create')" icon="plus"
label="Projekt für Unterstützung einreichen"/>
</div>
</div>
<!-- Filters -->
{{--<div class="mb-5">
<ul class="flex flex-wrap -m-1">
<li class="m-1">
<button class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800 transition">View All</button>
</li>
<li class="m-1">
<button class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 shadow-sm bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 transition">Online</button>
</li>
<li class="m-1">
<button class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 shadow-sm bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 transition">Local</button>
</li>
<li class="m-1">
<button class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 shadow-sm bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 transition">This Week</button>
</li>
<li class="m-1">
<button class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 shadow-sm bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 transition">This Month</button>
</li>
<li class="m-1">
<button class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 shadow-sm bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 transition">Following</button>
</li>
</ul>
</div>--}}
<div class="text-sm text-gray-500 dark:text-gray-400 italic mb-4">{{ count($projects) }} Projekte</div>
<!-- Content -->
<div class="grid xl:grid-cols-2 gap-6 mb-8">
@foreach($projects as $project)
<article
wire:key="project_{{ $project->id }}"
class="flex bg-white dark:bg-gray-800 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="{{ asset('einundzwanzig-alpha.jpg') }}" width="220" height="236" alt="Meetup 01">
<button class="absolute top-0 right-0 mt-4 mr-4">
<img class="rounded-full h-8 w-8" src="{{ $project->einundzwanzigPleb->profile->picture }}"
alt="">
</button>
</a>
<!-- Content -->
<div class="grow p-5 flex flex-col">
<div class="grow">
<div class="text-sm font-semibold text-amber-500 uppercase mb-2">
Eingereicht von: {{ $project->einundzwanzigPleb->profile->name }}
</div>
<a class="inline-flex mb-2">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100">
{{ $project->name }}
</h3>
</a>
<div class="text-sm">
{!! strip_tags($project->description) !!}
</div>
</div>
<!-- Footer -->
<div class="flex justify-between items-center mt-3">
<!-- Tag -->
<div
class="text-xs inline-flex items-center font-bold border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-200 rounded-full text-center px-2.5 py-1">
<span>{{ number_format($project->support_in_sats, 0, ',', '.') }} Sats</span>
</div>
<!-- Avatars -->
@if($project->votes->count() > 0)
<div class="flex items-center space-x-2">
<div class="text-xs font-medium text-gray-400 dark:text-gray-300 italic">
Anzahl der Unterstützer: +{{ $project->votes->count() }}
</div>
</div>
@endif
</div>
</div>
</article>
@endforeach
</div>
</div>
@endvolt
</x-layouts.app>

View File

@@ -8,12 +8,9 @@ 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 Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{on};
use function Livewire\Volt\{state, mount, on, computed};
name('welcome');