mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-11 02:50:29 +00:00
🔥 **Remove Highscore and Bindle features**
- 🗑️ Deleted `Highscore` feature (Model, Controller, Factory, Tests, Routes, Migrations) and associated logic. - 🗑️ Removed `BindleController` and its related test. - 🧹 Cleaned up unused routes, database seeders, and localization references. - 🚫 Deprecated inactive book rental guide component and associated views.
This commit is contained in:
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\LibraryItem;
|
|
||||||
use Dedoc\Scramble\Attributes\Group;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
#[Group(name: 'Community', weight: 7)]
|
|
||||||
class BindleController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Bindles (Bibliotheks-Einträge) auflisten
|
|
||||||
*
|
|
||||||
* Liefert die Bibliothekseinträge vom Typ 'bindle' mit id, name, link und image.
|
|
||||||
*
|
|
||||||
* @return Collection<int, array{id: int, name: string, link: string, image: string}>
|
|
||||||
*/
|
|
||||||
public function __invoke(): Collection
|
|
||||||
{
|
|
||||||
return LibraryItem::query()
|
|
||||||
->where('type', 'bindle')
|
|
||||||
->with([
|
|
||||||
'media',
|
|
||||||
])
|
|
||||||
->orderByDesc('id')
|
|
||||||
->get()
|
|
||||||
->map(fn ($item) => [
|
|
||||||
'id' => $item->id,
|
|
||||||
'name' => $item->name,
|
|
||||||
'link' => strtok($item->value, '?'),
|
|
||||||
'image' => $item->getFirstMediaUrl('main'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\StoreHighscoreRequest;
|
|
||||||
use App\Models\Highscore;
|
|
||||||
use Carbon\CarbonImmutable;
|
|
||||||
use Dedoc\Scramble\Attributes\Group;
|
|
||||||
use Dedoc\Scramble\Attributes\Response;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use swentel\nostr\Filter\Filter;
|
|
||||||
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;
|
|
||||||
|
|
||||||
#[Group(name: 'Highscores', weight: 6)]
|
|
||||||
class HighscoreController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Highscore-Bestenliste abrufen
|
|
||||||
*
|
|
||||||
* Öffentliche Bestenliste des Spiels, absteigend nach Satoshis (dann nach Zeitpunkt).
|
|
||||||
* Die Antwort hat die Form { data: [ { npub, name, satoshis, blocks, datetime } ] }.
|
|
||||||
*/
|
|
||||||
public function index(): JsonResponse
|
|
||||||
{
|
|
||||||
// npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6
|
|
||||||
$highscores = Highscore::query()
|
|
||||||
->orderByDesc('satoshis')
|
|
||||||
->orderBy('achieved_at')
|
|
||||||
->get()
|
|
||||||
->map(fn (Highscore $highscore) => [
|
|
||||||
'npub' => $highscore->npub,
|
|
||||||
'name' => $highscore->name,
|
|
||||||
'satoshis' => $highscore->satoshis,
|
|
||||||
'blocks' => $highscore->blocks,
|
|
||||||
'datetime' => $highscore->achieved_at->toIso8601String(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $highscores,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highscore einreichen
|
|
||||||
*
|
|
||||||
* Reicht einen Highscore ein (idempotent pro npub und Zeitpunkt).
|
|
||||||
* Zusätzlich auf 10 Anfragen pro Minute begrenzt.
|
|
||||||
* Fehlt ein Name, versucht der Server, ihn über das Nostr-Profil zu ergänzen.
|
|
||||||
* Antwortet mit HTTP 202.
|
|
||||||
*/
|
|
||||||
#[Response(status: 429, description: 'Zu viele Anfragen (Limit: 10 pro Minute überschritten).')]
|
|
||||||
public function store(StoreHighscoreRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validated();
|
|
||||||
$achievedAt = CarbonImmutable::parse($validated['datetime']);
|
|
||||||
|
|
||||||
$highscore = Highscore::query()->firstOrNew([
|
|
||||||
'npub' => $validated['npub'],
|
|
||||||
'achieved_at' => $achievedAt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$highscore->satoshis = (int) $validated['satoshis'];
|
|
||||||
$highscore->blocks = (int) $validated['blocks'];
|
|
||||||
|
|
||||||
if (array_key_exists('name', $validated)) {
|
|
||||||
$highscore->name = $validated['name'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$highscore->save();
|
|
||||||
|
|
||||||
if (empty($highscore->name)) {
|
|
||||||
$fetchedName = $this->fetchNostrName($highscore->npub);
|
|
||||||
if ($fetchedName) {
|
|
||||||
$highscore->name = $fetchedName;
|
|
||||||
$highscore->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info('Highscore submission received', [
|
|
||||||
'npub' => $highscore->npub,
|
|
||||||
'name' => $highscore->name,
|
|
||||||
'satoshis' => $highscore->satoshis,
|
|
||||||
'blocks' => $highscore->blocks,
|
|
||||||
'datetime' => $highscore->achieved_at->toIso8601String(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Highscore received',
|
|
||||||
'data' => [
|
|
||||||
'npub' => $highscore->npub,
|
|
||||||
'name' => $highscore->name,
|
|
||||||
'satoshis' => $highscore->satoshis,
|
|
||||||
'blocks' => $highscore->blocks,
|
|
||||||
'datetime' => $highscore->achieved_at->toIso8601String(),
|
|
||||||
],
|
|
||||||
], 202);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function fetchNostrName(string $npub): ?string
|
|
||||||
{
|
|
||||||
$author = trim($npub);
|
|
||||||
|
|
||||||
if (! str_starts_with($author, 'npub1')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$subscription = new Subscription;
|
|
||||||
$filter = new Filter;
|
|
||||||
$filter->setAuthors([$author]);
|
|
||||||
$filter->setKinds([0]);
|
|
||||||
|
|
||||||
$requestMessage = new RequestMessage($subscription->getId(), [$filter]);
|
|
||||||
$relaySet = new RelaySet;
|
|
||||||
$relaySet->setRelays([
|
|
||||||
new Relay('wss://nos.lol'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$request = new Request($relaySet, $requestMessage);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = $request->send();
|
|
||||||
|
|
||||||
foreach ($response as $relayUrl => $relayResponses) {
|
|
||||||
foreach ($relayResponses as $message) {
|
|
||||||
if (! isset($message->event)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$profile = json_decode($message->event->content, true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
if (isset($profile['name']) && is_string($profile['name']) && $profile['name'] !== '') {
|
|
||||||
Log::info('Fetched nostr profile name for highscore', [
|
|
||||||
'npub' => $author,
|
|
||||||
'relay' => $relayUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $profile['name'];
|
|
||||||
}
|
|
||||||
} catch (\JsonException $e) {
|
|
||||||
Log::warning('Failed to decode nostr profile for highscore', [
|
|
||||||
'npub' => $author,
|
|
||||||
'relay' => $relayUrl,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::warning('Failed to fetch nostr profile for highscore', [
|
|
||||||
'npub' => $author,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreHighscoreRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function expectsJson(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'npub' => ['required', 'string', 'starts_with:npub1', 'max:100'],
|
|
||||||
'name' => ['nullable', 'string', 'max:255'],
|
|
||||||
'satoshis' => ['required', 'integer', 'min:0'],
|
|
||||||
'blocks' => ['required', 'integer', 'min:0'],
|
|
||||||
'datetime' => ['required', 'date'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'npub.required' => 'An npub is required to record the highscore.',
|
|
||||||
'npub.starts_with' => 'The npub must start with npub1.',
|
|
||||||
'name.string' => 'The name must be a valid string.',
|
|
||||||
'satoshis.required' => 'Please provide the earned satoshis amount.',
|
|
||||||
'satoshis.integer' => 'Satoshis must be a whole number.',
|
|
||||||
'blocks.required' => 'Please provide the number of blocks.',
|
|
||||||
'blocks.integer' => 'Blocks must be a whole number.',
|
|
||||||
'datetime.required' => 'Please provide when the score was achieved.',
|
|
||||||
'datetime.date' => 'The datetime value must be a valid date.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\BooksForPlebs;
|
|
||||||
|
|
||||||
use App\Traits\SeoTrait;
|
|
||||||
use Livewire\Component;
|
|
||||||
use RalphJSmit\Laravel\SEO\Support\SEOData;
|
|
||||||
|
|
||||||
class BookRentalGuide extends Component
|
|
||||||
{
|
|
||||||
use SeoTrait;
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.books-for-plebs.book-rental-guide')->with( [
|
|
||||||
'SEOData' => new SEOData(
|
|
||||||
title: __('BooksForPlebs'),
|
|
||||||
description: __('Lokale Buchausleihe für Bitcoin-Meetups.'),
|
|
||||||
image: asset('img/book-rental.jpg')
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Highscore extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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',
|
|
||||||
'satoshis' => 'integer',
|
|
||||||
'blocks' => 'integer',
|
|
||||||
'achieved_at' => 'datetime',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
+2
-3
@@ -64,8 +64,7 @@ return [
|
|||||||
|
|
||||||
## Rate Limiting
|
## Rate Limiting
|
||||||
|
|
||||||
Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt, das Einreichen von
|
Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt.
|
||||||
Highscores zusätzlich auf **10 Anfragen/Minute**.
|
|
||||||
MARKDOWN,
|
MARKDOWN,
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ return [
|
|||||||
'view' => 'scramble::scalar',
|
'view' => 'scramble::scalar',
|
||||||
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
|
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
|
||||||
'theme' => 'laravel',
|
'theme' => 'laravel',
|
||||||
'proxyUrl' => 'https://proxy.scalar.com',
|
'proxyUrl' => '',
|
||||||
'darkMode' => true,
|
'darkMode' => true,
|
||||||
'showDeveloperTools' => 'never',
|
'showDeveloperTools' => 'never',
|
||||||
'agent' => ['disabled' => true],
|
'agent' => ['disabled' => true],
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Factories;
|
|
||||||
|
|
||||||
use App\Models\Highscore;
|
|
||||||
use Database\Factories\Helpers\NostrHelper;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @extends Factory<Highscore>
|
|
||||||
*/
|
|
||||||
class HighscoreFactory extends Factory
|
|
||||||
{
|
|
||||||
protected $model = Highscore::class;
|
|
||||||
|
|
||||||
public function definition(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'npub' => NostrHelper::randomNpub(),
|
|
||||||
'name' => fake()->name(),
|
|
||||||
'satoshis' => fake()->numberBetween(0, 100000),
|
|
||||||
'blocks' => fake()->numberBetween(0, 1000),
|
|
||||||
'achieved_at' => fake()->dateTimeBetween('-1 year', 'now'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?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::create('highscores', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('npub', 100);
|
|
||||||
$table->string('name')->nullable();
|
|
||||||
$table->unsignedBigInteger('satoshis');
|
|
||||||
$table->unsignedInteger('blocks');
|
|
||||||
$table->dateTime('achieved_at');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique(['npub', 'achieved_at']);
|
|
||||||
$table->index('satoshis');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('highscores');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -11,7 +11,6 @@ use App\Models\CourseEvent;
|
|||||||
use App\Models\EmailCampaign;
|
use App\Models\EmailCampaign;
|
||||||
use App\Models\EmailTexts;
|
use App\Models\EmailTexts;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Highscore;
|
|
||||||
use App\Models\Lecturer;
|
use App\Models\Lecturer;
|
||||||
use App\Models\Library;
|
use App\Models\Library;
|
||||||
use App\Models\LibraryItem;
|
use App\Models\LibraryItem;
|
||||||
@@ -28,7 +27,6 @@ use App\Models\TwitterAccount;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Venue;
|
use App\Models\Venue;
|
||||||
use App\Models\Vote;
|
use App\Models\Vote;
|
||||||
use Database\Factories\Helpers\NostrHelper;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -167,7 +165,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->command->info('Phase 6: Voting & Highscores');
|
$this->command->info('Phase 6: Voting');
|
||||||
$proposals->each(function (ProjectProposal $proposal) use ($users) {
|
$proposals->each(function (ProjectProposal $proposal) use ($users) {
|
||||||
foreach ($users->random(min(8, $users->count())) as $voter) {
|
foreach ($users->random(min(8, $users->count())) as $voter) {
|
||||||
Vote::create([
|
Vote::create([
|
||||||
@@ -180,17 +178,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (NostrHelper::realNpubs() as $i => $npub) {
|
|
||||||
for ($d = 0; $d < 5; $d++) {
|
|
||||||
Highscore::factory()->create([
|
|
||||||
'npub' => $npub,
|
|
||||||
'achieved_at' => now()->subDays(($i * 10) + $d),
|
|
||||||
'satoshis' => fake()->numberBetween(1000, 1_000_000),
|
|
||||||
'blocks' => fake()->numberBetween(1, 5000),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->command->info('Phase 7: LoginKeys');
|
$this->command->info('Phase 7: LoginKeys');
|
||||||
LoginKey::factory()->count(5)->recycle($users)->create();
|
LoginKey::factory()->count(5)->recycle($users)->create();
|
||||||
|
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
<div class="h-screen w-full">
|
|
||||||
<div class="px-4 md:px-8 lg:px-24 py-5">
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center mb-8">
|
|
||||||
<h1 class="text-4xl md:text-5xl text-orange-500 mb-4 md:mb-0">
|
|
||||||
Anleitung zum Bücherverleih
|
|
||||||
</h1>
|
|
||||||
<img src="{{ asset('/img/apple_touch_icon.png') }}" alt="Buch Etiketten"
|
|
||||||
class="object-cover h-32 rounded-md shadow-md">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-2xl md:text-3xl mb-4 text-white">
|
|
||||||
Hallo Pleb,
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="text-lg mb-8 text-white">
|
|
||||||
Vielen Dank, dass du dich dazu entschieden hast, deine
|
|
||||||
<span class="text-orange-500">
|
|
||||||
₿itcoin-Bücher
|
|
||||||
</span>
|
|
||||||
zur Verfügung zu stellen. Mit dieser Anleitung kannst du eine Bezahladresse
|
|
||||||
generieren und hast auch alle Materialien, die du benötigst. Wir haben
|
|
||||||
darauf geachtet, dass es für jedes Meetup geeignet ist. Deshalb stellen
|
|
||||||
wir dir die Quelldateien zur Verfügung, damit du deinen eigenen QR-Code
|
|
||||||
einfügen und das Logo eures Meetups verwenden kannst.
|
|
||||||
<br>
|
|
||||||
<p class="text-lg text-white mt-8">
|
|
||||||
Du hast keine ₿itcoin Wallet oder kein Programm zum Bearbeiten der Dateien?
|
|
||||||
Kein Problem! Schreib uns einfach und wir helfen dir.
|
|
||||||
</p>
|
|
||||||
<br>
|
|
||||||
<a href="https://t.me/Awesomo12" target="_blank" class="text-orange-500 underline telegram-blue">
|
|
||||||
<i class="fab fa-telegram mr-2">
|
|
||||||
</i>
|
|
||||||
@Awesomo12
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<a href="https://t.me/LottiTheFuchs" target="_blank" class="text-orange-500 underline telegram-blue">
|
|
||||||
<i class="fab fa-telegram mr-2">
|
|
||||||
</i>
|
|
||||||
@LottiTheFuchs
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<a href="https://t.me/Robin_Hodl21" target="_blank" class="text-orange-500 underline telegram-blue">
|
|
||||||
<i class="fab fa-telegram mr-2">
|
|
||||||
</i>
|
|
||||||
@Robin_Hodl21
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<!-- Bücheretiketten -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<h2 class="text-2xl mb-2 text-orange-500">
|
|
||||||
Bücheretiketten
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-gray-300 mb-2">
|
|
||||||
(Zum Editieren brauchst du
|
|
||||||
<a href="https://www.adobe.com/de/products/illustrator.html" target="_blank"
|
|
||||||
class="text-orange-500 underline link-gray">
|
|
||||||
Adobe Illustrator)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<img src="{{ asset('/img/etikett_bucherVerleih-min.jpg') }}" alt="Buch Etiketten"
|
|
||||||
class="mb-4 object-cover h-64 rounded-md shadow-md">
|
|
||||||
<div class="flex justify-center space-x-2">
|
|
||||||
|
|
||||||
<a download="true" href="{{ route('buecherverleih.download', ['filename' => 'buecherverleih.zip']) }}" class="btn bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700">
|
|
||||||
<p class="text-white">
|
|
||||||
Download .zip
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Flyer -->
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<h2 class="text-2xl mb-2 text-orange-500">
|
|
||||||
Flyer
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-gray-300 mb-2">
|
|
||||||
(Zum Editieren brauchst du
|
|
||||||
<a href="https://www.adobe.com/de/products/illustrator.html" target="_blank"
|
|
||||||
class="text-orange-500 underline link-gray">
|
|
||||||
Adobe Illustrator)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<img src="{{ asset('/img/flyerBuecherverleih-min.jpg') }}" alt="Flyer" class="mb-4 object-cover h-64 rounded-md shadow-md">
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a download="true" href="{{ route('buecherverleih.download', ['filename' => 'buecherverleih.zip']) }}" class="btn bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700">
|
|
||||||
<p class="text-white">
|
|
||||||
Download .zip
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Lesezeichen -->
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<h2 class="text-2xl mb-2 text-orange-500">
|
|
||||||
Lesezeichen
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-gray-300 mb-2">
|
|
||||||
(Nicht editierbar)
|
|
||||||
</p>
|
|
||||||
<img src="{{ asset('/img/Lesezeichen-min.jpg') }}" alt="Flyer" class="mb-4 object-cover h-64 rounded-md shadow-md">
|
|
||||||
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<flux:button
|
|
||||||
variant="primary"
|
|
||||||
download="Lesezeichen"
|
|
||||||
:href="asset('/img/Lesezeichen-min.jpg')">
|
|
||||||
Download .jpg
|
|
||||||
</flux:button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bookring4Sats -->
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<h2 class="text-2xl mb-2 text-orange-500">
|
|
||||||
Bookring4Sats
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-gray-300 mb-2">
|
|
||||||
(Nicht editierbar)
|
|
||||||
</p>
|
|
||||||
<img src="{{ asset('/img/B4S-min.jpg') }}" alt="Flyer" class="mb-4 object-cover h-64 rounded-md shadow-md">
|
|
||||||
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<flux:button
|
|
||||||
variant="primary"
|
|
||||||
download="B4S"
|
|
||||||
:href="asset('/img/B4S-min.jpg')">
|
|
||||||
Download .jpg
|
|
||||||
</flux:button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-lg mb-8 text-white">
|
|
||||||
Um deinen
|
|
||||||
<span class="text-orange-500">
|
|
||||||
₿itcoin
|
|
||||||
</span>
|
|
||||||
QR-Code zu erstellen, kopiere einfach die Empfangsadresse aus der Wallet
|
|
||||||
deiner Wahl und füge sie hier ein:
|
|
||||||
<br>
|
|
||||||
<a href="https://www.qr-code-generator.com/" target="_blank" class="text-orange-500 underline">
|
|
||||||
www.qr-code-generator.com
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-lg text-white font-bold mb-8">
|
|
||||||
Der QR-Code-Generator akzeptiert sowohl Lightning als auch Onchain-Empfangsadressen.
|
|
||||||
Wir empfehlen dir jedoch Lightning zu verwenden, da es schneller und zumeist deutlich günstiger als Onchain ist.
|
|
||||||
<br>
|
|
||||||
<span class="flex items-center text-red-500">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-2">
|
|
||||||
</i>
|
|
||||||
Vorsicht: Bitte KEINE Lightning-Adressen,verwenden, da diese nach einem bestimmten Zeitraum ablaufen!
|
|
||||||
Stattdessen nutzt Ihr bitte eine LNURL, da diese Statisch sind.
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Tipp: Der Lightning TipBot "LN.tips" erzeugt euch eine. Dazu müsst Ihr diesen nur aktivieren und mit dem Befehl
|
|
||||||
"/advanced" eure LNURL anzeigen lassen und diese copy-pasten.
|
|
||||||
Alternativ kann dies auch die WalletOfSatoshi, dort sehen Sie aus wie E-Mail Adessen, wie zum Beispiel:
|
|
||||||
"BitcoinKalle@walletofsatoshi.com"
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-lg text-white">
|
|
||||||
Für die sichere Lagerung deiner Bücher empfehlen wir einen Meetup-Ort,
|
|
||||||
an dem du regelmäßig bist und die Bücher auch sicher verstaut werden können.
|
|
||||||
Falls dem nicht der Fall ist, solltet Ihr die Bücher lieber jedes mal separat mit zum Meetup
|
|
||||||
nehmen und die nicht verliehenen Bücher auch wieder mit zurück.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-lg text-white mt-8">
|
|
||||||
Du willst deine Bücher nicht nur deinem lokalen Meetup zur Verfügung stellen,
|
|
||||||
sondern online an die gesamte Community verschicken, dann komm in die Gruppe:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-lg text-white">
|
|
||||||
<a href="https://t.me/BOOKRING4SATS" target="_blank" class="text-orange-500 underline telegram-blue">
|
|
||||||
<br>
|
|
||||||
<i class="fab fa-telegram mr-2">
|
|
||||||
</i>
|
|
||||||
@BOOKRING4SATS
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-lg mt-8 text-white font-bold">
|
|
||||||
<i class="fas fa-book mr-2 text-orange-500">
|
|
||||||
</i>
|
|
||||||
Vielen Dank, dass du deine Bücher zur Verfügung stellst und uns dabei
|
|
||||||
hilfst, das Wissen über ₿itcoin zu verbreiten!
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center mt-4">
|
|
||||||
<img src="/img/btc-logo-6219386_1280.png" class="h-16" alt="">
|
|
||||||
<span class="text-orange-500">
|
|
||||||
Happy Stacking
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Api\BindleController;
|
|
||||||
use App\Http\Controllers\Api\BtcMapCommunityController;
|
use App\Http\Controllers\Api\BtcMapCommunityController;
|
||||||
use App\Http\Controllers\Api\CityController;
|
use App\Http\Controllers\Api\CityController;
|
||||||
use App\Http\Controllers\Api\CountryController;
|
use App\Http\Controllers\Api\CountryController;
|
||||||
use App\Http\Controllers\Api\CourseController;
|
use App\Http\Controllers\Api\CourseController;
|
||||||
use App\Http\Controllers\Api\CourseEventController;
|
use App\Http\Controllers\Api\CourseEventController;
|
||||||
use App\Http\Controllers\Api\HighscoreController;
|
|
||||||
use App\Http\Controllers\Api\LecturerController;
|
use App\Http\Controllers\Api\LecturerController;
|
||||||
use App\Http\Controllers\Api\MeetupController;
|
use App\Http\Controllers\Api\MeetupController;
|
||||||
use App\Http\Controllers\Api\MeetupEventController;
|
use App\Http\Controllers\Api\MeetupEventController;
|
||||||
@@ -27,12 +25,7 @@ Route::middleware(['throttle:60,1'])
|
|||||||
->only(['index', 'show']);
|
->only(['index', 'show']);
|
||||||
Route::resource('cities', CityController::class);
|
Route::resource('cities', CityController::class);
|
||||||
Route::resource('venues', VenueController::class);
|
Route::resource('venues', VenueController::class);
|
||||||
Route::get('highscores', [HighscoreController::class, 'index'])->name('highscores.index');
|
|
||||||
Route::post('highscores', [HighscoreController::class, 'store'])
|
|
||||||
->middleware('throttle:10,1')
|
|
||||||
->name('highscores.store');
|
|
||||||
Route::get('nostrplebs', NostrPlebController::class);
|
Route::get('nostrplebs', NostrPlebController::class);
|
||||||
Route::get('bindles', BindleController::class);
|
|
||||||
Route::get('meetups', MeetupMapController::class);
|
Route::get('meetups', MeetupMapController::class);
|
||||||
Route::get('meetup-events/{date?}', MeetupEventController::class);
|
Route::get('meetup-events/{date?}', MeetupEventController::class);
|
||||||
Route::get('btc-map-communities', BtcMapCommunityController::class);
|
Route::get('btc-map-communities', BtcMapCommunityController::class);
|
||||||
|
|||||||
@@ -16,27 +16,6 @@ Route::get('error/{code}', function (string $code) {
|
|||||||
abort($code >= 400 && $code <= 599 ? $code : 404);
|
abort($code >= 400 && $code <= 599 ? $code : 404);
|
||||||
})->where('code', '[0-9]{3}');
|
})->where('code', '[0-9]{3}');
|
||||||
|
|
||||||
/*
|
|
||||||
* Commented out routes related to book rental download and display
|
|
||||||
* These are currently inactive but can be enabled if needed
|
|
||||||
*/
|
|
||||||
/*Route::get('/download-buecherverleih', function (Request $request) {
|
|
||||||
$filename = $request->input('filename');
|
|
||||||
// Get the file path from the storage folder
|
|
||||||
$filePath = storage_path('app/'.$filename);
|
|
||||||
dd($filePath);
|
|
||||||
// Check if the file exists
|
|
||||||
if (!file_exists($filePath)) {
|
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
// Generate a response with the file for download
|
|
||||||
return response()->download($filePath, $filename);
|
|
||||||
})->name('buecherverleih.download');
|
|
||||||
|
|
||||||
Route::middleware([])
|
|
||||||
->get('/buecherverleih', \App\Livewire\BooksForPlebs\BookRentalGuide::class)
|
|
||||||
->name('buecherverleih');*/
|
|
||||||
|
|
||||||
// Route for rabbit following helper page - Updated for Livewire v4
|
// Route for rabbit following helper page - Updated for Livewire v4
|
||||||
Route::livewire('/kaninchenbau', FollowTheRabbit::class)
|
Route::livewire('/kaninchenbau', FollowTheRabbit::class)
|
||||||
->name('kaninchenbau');
|
->name('kaninchenbau');
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Models\Highscore;
|
|
||||||
|
|
||||||
it('returns all highscores ordered by satoshis desc on GET /api/highscores', function () {
|
|
||||||
Highscore::factory()->create(['satoshis' => 100, 'achieved_at' => now()->subHours(1)]);
|
|
||||||
Highscore::factory()->create(['satoshis' => 5000, 'achieved_at' => now()->subHours(2)]);
|
|
||||||
Highscore::factory()->create(['satoshis' => 1000, 'achieved_at' => now()->subHours(3)]);
|
|
||||||
|
|
||||||
$response = $this->getJson('/api/highscores');
|
|
||||||
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$data = $response->json('data');
|
|
||||||
expect(collect($data)->pluck('satoshis')->all())->toBe([5000, 1000, 100]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts a valid highscore submission', function () {
|
|
||||||
$payload = [
|
|
||||||
'npub' => 'npub1'.str_repeat('a', 58),
|
|
||||||
'name' => 'Tester',
|
|
||||||
'satoshis' => 1234,
|
|
||||||
'blocks' => 5,
|
|
||||||
'datetime' => now()->subDay()->toIso8601String(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->postJson('/api/highscores', $payload)
|
|
||||||
->assertStatus(202)
|
|
||||||
->assertJsonPath('data.satoshis', 1234)
|
|
||||||
->assertJsonPath('data.name', 'Tester');
|
|
||||||
|
|
||||||
expect(Highscore::query()->where('npub', $payload['npub'])->exists())->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects a highscore submission missing npub', function () {
|
|
||||||
$this->postJson('/api/highscores', [
|
|
||||||
'satoshis' => 1234,
|
|
||||||
'blocks' => 5,
|
|
||||||
'datetime' => now()->toIso8601String(),
|
|
||||||
])->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors(['npub']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects a highscore submission with an npub that does not start with npub1', function () {
|
|
||||||
$this->postJson('/api/highscores', [
|
|
||||||
'npub' => 'nsec1'.str_repeat('a', 58),
|
|
||||||
'satoshis' => 1234,
|
|
||||||
'blocks' => 5,
|
|
||||||
'datetime' => now()->toIso8601String(),
|
|
||||||
])->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors(['npub']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects a highscore submission with negative satoshis', function () {
|
|
||||||
$this->postJson('/api/highscores', [
|
|
||||||
'npub' => 'npub1'.str_repeat('b', 58),
|
|
||||||
'satoshis' => -10,
|
|
||||||
'blocks' => 5,
|
|
||||||
'datetime' => now()->toIso8601String(),
|
|
||||||
])->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors(['satoshis']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects a highscore submission with an invalid datetime', function () {
|
|
||||||
$this->postJson('/api/highscores', [
|
|
||||||
'npub' => 'npub1'.str_repeat('c', 58),
|
|
||||||
'satoshis' => 100,
|
|
||||||
'blocks' => 5,
|
|
||||||
'datetime' => 'not-a-date',
|
|
||||||
])->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors(['datetime']);
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\LibraryItem;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
it('returns nostr-pubkeys in /api/nostrplebs', function () {
|
it('returns nostr-pubkeys in /api/nostrplebs', function () {
|
||||||
@@ -15,16 +14,3 @@ it('returns nostr-pubkeys in /api/nostrplebs', function () {
|
|||||||
->toHaveCount(2)
|
->toHaveCount(2)
|
||||||
->each->toStartWith('npub1');
|
->each->toStartWith('npub1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns bindle-type library items in /api/bindles', function () {
|
|
||||||
LibraryItem::factory()->create(['type' => 'bindle', 'name' => 'My Bindle']);
|
|
||||||
LibraryItem::factory()->create(['type' => 'article', 'name' => 'My Article']);
|
|
||||||
|
|
||||||
$response = $this->getJson('/api/bindles');
|
|
||||||
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$names = collect($response->json())->pluck('name');
|
|
||||||
expect($names->all())
|
|
||||||
->toContain('My Bindle')
|
|
||||||
->not->toContain('My Article');
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Livewire\BooksForPlebs\BookRentalGuide;
|
|
||||||
use Illuminate\View\ViewException;
|
|
||||||
use Livewire\Livewire;
|
|
||||||
|
|
||||||
it('mounts the BookRentalGuide component but its view references a route that is currently commented out in routes/web.php', function () {
|
|
||||||
expect(fn () => Livewire::test(BookRentalGuide::class)->assertStatus(200))
|
|
||||||
->toThrow(ViewException::class, 'Route [buecherverleih.download] not defined.');
|
|
||||||
})->skip('Component is unreachable: /buecherverleih route is commented out in routes/web.php — view references the missing buecherverleih.download route.');
|
|
||||||
|
|
||||||
it('confirms the BookRentalGuide component class still exists', function () {
|
|
||||||
expect(class_exists(BookRentalGuide::class))->toBeTrue();
|
|
||||||
});
|
|
||||||
@@ -9,7 +9,6 @@ use App\Models\CourseEvent;
|
|||||||
use App\Models\EmailCampaign;
|
use App\Models\EmailCampaign;
|
||||||
use App\Models\EmailTexts;
|
use App\Models\EmailTexts;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Highscore;
|
|
||||||
use App\Models\Lecturer;
|
use App\Models\Lecturer;
|
||||||
use App\Models\Library;
|
use App\Models\Library;
|
||||||
use App\Models\LibraryItem;
|
use App\Models\LibraryItem;
|
||||||
@@ -62,7 +61,6 @@ it('creates a valid persisted record via the factory', function (string $modelCl
|
|||||||
'Participant' => Participant::class,
|
'Participant' => Participant::class,
|
||||||
'EmailCampaign' => EmailCampaign::class,
|
'EmailCampaign' => EmailCampaign::class,
|
||||||
'EmailTexts' => EmailTexts::class,
|
'EmailTexts' => EmailTexts::class,
|
||||||
'Highscore' => Highscore::class,
|
|
||||||
'LoginKey' => LoginKey::class,
|
'LoginKey' => LoginKey::class,
|
||||||
'Tag' => Tag::class,
|
'Tag' => Tag::class,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ use App\Models\City;
|
|||||||
use App\Models\Country;
|
use App\Models\Country;
|
||||||
use App\Models\Course;
|
use App\Models\Course;
|
||||||
use App\Models\CourseEvent;
|
use App\Models\CourseEvent;
|
||||||
use App\Models\Highscore;
|
|
||||||
use App\Models\Lecturer;
|
use App\Models\Lecturer;
|
||||||
use App\Models\LibraryItem;
|
|
||||||
use App\Models\Meetup;
|
use App\Models\Meetup;
|
||||||
use App\Models\MeetupEvent;
|
use App\Models\MeetupEvent;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -21,8 +19,6 @@ beforeEach(function () {
|
|||||||
Course::factory()->create();
|
Course::factory()->create();
|
||||||
CourseEvent::factory()->create();
|
CourseEvent::factory()->create();
|
||||||
Lecturer::factory()->create();
|
Lecturer::factory()->create();
|
||||||
Highscore::factory()->create();
|
|
||||||
LibraryItem::factory()->create(['type' => 'bindle']);
|
|
||||||
User::factory()->create(['nostr' => 'npub1'.str_repeat('a', 58)]);
|
User::factory()->create(['nostr' => 'npub1'.str_repeat('a', 58)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,12 +30,10 @@ it('returns a JSON response for the API GET endpoint', function (string $path) {
|
|||||||
'meetup events' => '/api/meetup-events',
|
'meetup events' => '/api/meetup-events',
|
||||||
'btc-map communities' => '/api/btc-map-communities',
|
'btc-map communities' => '/api/btc-map-communities',
|
||||||
'nostrplebs' => '/api/nostrplebs',
|
'nostrplebs' => '/api/nostrplebs',
|
||||||
'bindles' => '/api/bindles',
|
|
||||||
'lecturers' => '/api/lecturers',
|
'lecturers' => '/api/lecturers',
|
||||||
'courses' => '/api/courses',
|
'courses' => '/api/courses',
|
||||||
'cities' => '/api/cities',
|
'cities' => '/api/cities',
|
||||||
'venues' => '/api/venues',
|
'venues' => '/api/venues',
|
||||||
'highscores' => '/api/highscores',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function () {
|
it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user