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
|
||||
|
||||
Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt, das Einreichen von
|
||||
Highscores zusätzlich auf **10 Anfragen/Minute**.
|
||||
Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt.
|
||||
MARKDOWN,
|
||||
],
|
||||
|
||||
@@ -96,7 +95,7 @@ return [
|
||||
'view' => 'scramble::scalar',
|
||||
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
|
||||
'theme' => 'laravel',
|
||||
'proxyUrl' => 'https://proxy.scalar.com',
|
||||
'proxyUrl' => '',
|
||||
'darkMode' => true,
|
||||
'showDeveloperTools' => 'never',
|
||||
'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\EmailTexts;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Highscore;
|
||||
use App\Models\Lecturer;
|
||||
use App\Models\Library;
|
||||
use App\Models\LibraryItem;
|
||||
@@ -28,7 +27,6 @@ use App\Models\TwitterAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Venue;
|
||||
use App\Models\Vote;
|
||||
use Database\Factories\Helpers\NostrHelper;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\App;
|
||||
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) {
|
||||
foreach ($users->random(min(8, $users->count())) as $voter) {
|
||||
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');
|
||||
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
|
||||
|
||||
use App\Http\Controllers\Api\BindleController;
|
||||
use App\Http\Controllers\Api\BtcMapCommunityController;
|
||||
use App\Http\Controllers\Api\CityController;
|
||||
use App\Http\Controllers\Api\CountryController;
|
||||
use App\Http\Controllers\Api\CourseController;
|
||||
use App\Http\Controllers\Api\CourseEventController;
|
||||
use App\Http\Controllers\Api\HighscoreController;
|
||||
use App\Http\Controllers\Api\LecturerController;
|
||||
use App\Http\Controllers\Api\MeetupController;
|
||||
use App\Http\Controllers\Api\MeetupEventController;
|
||||
@@ -27,12 +25,7 @@ Route::middleware(['throttle:60,1'])
|
||||
->only(['index', 'show']);
|
||||
Route::resource('cities', CityController::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('bindles', BindleController::class);
|
||||
Route::get('meetups', MeetupMapController::class);
|
||||
Route::get('meetup-events/{date?}', MeetupEventController::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);
|
||||
})->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::livewire('/kaninchenbau', FollowTheRabbit::class)
|
||||
->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
|
||||
|
||||
use App\Models\LibraryItem;
|
||||
use App\Models\User;
|
||||
|
||||
it('returns nostr-pubkeys in /api/nostrplebs', function () {
|
||||
@@ -15,16 +14,3 @@ it('returns nostr-pubkeys in /api/nostrplebs', function () {
|
||||
->toHaveCount(2)
|
||||
->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\EmailTexts;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Highscore;
|
||||
use App\Models\Lecturer;
|
||||
use App\Models\Library;
|
||||
use App\Models\LibraryItem;
|
||||
@@ -62,7 +61,6 @@ it('creates a valid persisted record via the factory', function (string $modelCl
|
||||
'Participant' => Participant::class,
|
||||
'EmailCampaign' => EmailCampaign::class,
|
||||
'EmailTexts' => EmailTexts::class,
|
||||
'Highscore' => Highscore::class,
|
||||
'LoginKey' => LoginKey::class,
|
||||
'Tag' => Tag::class,
|
||||
]);
|
||||
|
||||
@@ -4,9 +4,7 @@ use App\Models\City;
|
||||
use App\Models\Country;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseEvent;
|
||||
use App\Models\Highscore;
|
||||
use App\Models\Lecturer;
|
||||
use App\Models\LibraryItem;
|
||||
use App\Models\Meetup;
|
||||
use App\Models\MeetupEvent;
|
||||
use App\Models\User;
|
||||
@@ -21,8 +19,6 @@ beforeEach(function () {
|
||||
Course::factory()->create();
|
||||
CourseEvent::factory()->create();
|
||||
Lecturer::factory()->create();
|
||||
Highscore::factory()->create();
|
||||
LibraryItem::factory()->create(['type' => 'bindle']);
|
||||
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',
|
||||
'btc-map communities' => '/api/btc-map-communities',
|
||||
'nostrplebs' => '/api/nostrplebs',
|
||||
'bindles' => '/api/bindles',
|
||||
'lecturers' => '/api/lecturers',
|
||||
'courses' => '/api/courses',
|
||||
'cities' => '/api/cities',
|
||||
'venues' => '/api/venues',
|
||||
'highscores' => '/api/highscores',
|
||||
]);
|
||||
|
||||
it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function () {
|
||||
|
||||
Reference in New Issue
Block a user