mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-01-25 04:13:17 +00:00
🗑️ Remove election-related blade files no longer in use
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,3 +19,4 @@ yarn-error.log
|
||||
/.idea
|
||||
/.vscode
|
||||
/relay
|
||||
/storage/media-library
|
||||
|
||||
375
.junie/guidelines.md
Normal file
375
.junie/guidelines.md
Normal file
@@ -0,0 +1,375 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3.29
|
||||
- laravel/folio (FOLIO) - v1
|
||||
- laravel/framework (LARAVEL) - v11
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/pulse (PULSE) - v1
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- livewire/livewire (LIVEWIRE) - v4
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- pestphp/pest (PEST) - v2
|
||||
- phpunit/phpunit (PHPUNIT) - v10
|
||||
- laravel-echo (ECHO) - v1
|
||||
- tailwindcss (TAILWINDCSS) - v3
|
||||
|
||||
## Conventions
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail yarn run build`, `vendor/bin/sail yarn run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
## Replies
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
|
||||
## URLs
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||
- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
=== sail rules ===
|
||||
|
||||
## Laravel Sail
|
||||
|
||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
|
||||
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||
- Install Composer packages: `vendor/bin/sail composer install`
|
||||
- Execute Node commands: `vendor/bin/sail yarn run dev`
|
||||
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||
|
||||
=== folio/core rules ===
|
||||
|
||||
## Laravel Folio
|
||||
|
||||
- Laravel Folio is a file-based router. With Laravel Folio, a new route is created for every Blade file within the configured Folio directory. For example, pages are usually in `resources/views/pages/` and the file structure determines routes:
|
||||
- `pages/index.blade.php` → `/`
|
||||
- `pages/profile/index.blade.php` → `/profile`
|
||||
- `pages/auth/login.blade.php` → `/auth/login`
|
||||
- You may list available Folio routes using `vendor/bin/sail artisan folio:list` or using the `list-routes` tool.
|
||||
|
||||
### New Pages & Routes
|
||||
- Always create new `folio` pages and routes using `vendor/bin/sail artisan folio:page [name]` following existing naming conventions.
|
||||
|
||||
<code-snippet name="Example folio:page Commands for Automatic Routing" lang="shell">
|
||||
// Creates: resources/views/pages/products.blade.php → /products
|
||||
vendor/bin/sail artisan folio:page "products"
|
||||
|
||||
// Creates: resources/views/pages/products/[id].blade.php → /products/{id}
|
||||
vendor/bin/sail artisan folio:page "products/[id]"
|
||||
</code-snippet>
|
||||
|
||||
- Add a 'name' to each new Folio page at the very top of the file so it has a named route available for other parts of the codebase to use.
|
||||
|
||||
<code-snippet name="Adding Named Route to Folio Page" lang="php">
|
||||
use function Laravel\Folio\name;
|
||||
|
||||
name('products.index');
|
||||
</code-snippet>
|
||||
|
||||
### Support & Documentation
|
||||
- Folio supports: middleware, serving pages from multiple paths, subdomain routing, named routes, nested routes, index routes, route parameters, and route model binding.
|
||||
- If available, use the `search-docs` tool to use Folio to its full potential and help the user effectively.
|
||||
|
||||
<code-snippet name="Folio Middleware Example" lang="php">
|
||||
use function Laravel\Folio\{name, middleware};
|
||||
|
||||
name('admin.products');
|
||||
middleware(['auth', 'verified', 'can:manage-products']);
|
||||
?>
|
||||
</code-snippet>
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
## Do Things the Laravel Way
|
||||
|
||||
- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Database
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
### Controllers & Validation
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
### URL Generation
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
### Configuration
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
### Testing
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
### Vite Error
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail yarn run build` or ask the user to run `vendor/bin/sail yarn run dev` or `vendor/bin/sail composer run dev`.
|
||||
|
||||
=== laravel/v11 rules ===
|
||||
|
||||
## Laravel 11
|
||||
|
||||
- Use the `search-docs` tool to get version-specific documentation.
|
||||
- Laravel 11 brought a new streamlined file structure which this project now uses.
|
||||
|
||||
### Laravel 11 Structure
|
||||
- In Laravel 11, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
### Database
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
### New Artisan Commands
|
||||
- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11:
|
||||
- `vendor/bin/sail artisan make:enum`
|
||||
- `vendor/bin/sail artisan make:class`
|
||||
- `vendor/bin/sail artisan make:interface`
|
||||
|
||||
=== livewire/core rules ===
|
||||
|
||||
## Livewire
|
||||
|
||||
- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests.
|
||||
- Use the `vendor/bin/sail artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
||||
- State should live on the server, with the UI reflecting it.
|
||||
- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions.
|
||||
|
||||
## Livewire Best Practices
|
||||
- Livewire components require a single root element.
|
||||
- Use `wire:loading` and `wire:dirty` for delightful loading states.
|
||||
- Add `wire:key` in loops:
|
||||
|
||||
```blade
|
||||
@foreach ($items as $item)
|
||||
<div wire:key="item-{{ $item->id }}">
|
||||
{{ $item->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
|
||||
|
||||
<code-snippet name="Lifecycle Hook Examples" lang="php">
|
||||
public function mount(User $user) { $this->user = $user; }
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
</code-snippet>
|
||||
|
||||
## Testing Livewire
|
||||
|
||||
<code-snippet name="Example Livewire Component Test" lang="php">
|
||||
Livewire::test(Counter::class)
|
||||
->assertSet('count', 0)
|
||||
->call('increment')
|
||||
->assertSet('count', 1)
|
||||
->assertSee(1)
|
||||
->assertStatus(200);
|
||||
</code-snippet>
|
||||
|
||||
<code-snippet name="Testing Livewire Component Exists on Page" lang="php">
|
||||
$this->get('/posts/create')
|
||||
->assertSeeLivewire(CreatePost::class);
|
||||
</code-snippet>
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
## Laravel Pint Code Formatter
|
||||
|
||||
- You must run `vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/sail bin pint --test`, simply run `vendor/bin/sail bin pint` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
### Testing
|
||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||
|
||||
### Pest Tests
|
||||
- All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||
- Pest tests look and behave like this:
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||
- To run all tests: `vendor/bin/sail artisan test --compact`.
|
||||
- To run all tests in a file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `vendor/bin/sail artisan test --compact --filter=testName` (recommended after making a change to a related file).
|
||||
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
|
||||
|
||||
### Pest Assertions
|
||||
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
|
||||
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
|
||||
it('returns all', function () {
|
||||
$response = $this->postJson('/api/docs', []);
|
||||
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Mocking
|
||||
- Mocking can be very helpful when appropriate.
|
||||
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
|
||||
- You can also create partial mocks using the same import or self method.
|
||||
|
||||
### Datasets
|
||||
- Use datasets in Pest to simplify tests that have a lot of duplicated data. This is often the case when testing validation rules, so consider this solution when writing tests for validation rules.
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
</code-snippet>
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.).
|
||||
- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically.
|
||||
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
|
||||
|
||||
### Spacing
|
||||
- When listing items, use gap utilities for spacing; don't use margins.
|
||||
|
||||
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Superior</div>
|
||||
<div>Michigan</div>
|
||||
<div>Erie</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Dark Mode
|
||||
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
|
||||
|
||||
=== tailwindcss/v3 rules ===
|
||||
|
||||
## Tailwind CSS 3
|
||||
|
||||
- Always use Tailwind CSS v3; verify you're using only classes supported by this version.
|
||||
</laravel-boost-guidelines>
|
||||
11
.junie/mcp/mcp.json
Normal file
11
.junie/mcp/mcp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "vendor/bin/sail",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
.mcp.json
Normal file
11
.mcp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "vendor/bin/sail",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
375
AGENTS.md
Normal file
375
AGENTS.md
Normal file
@@ -0,0 +1,375 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3.29
|
||||
- laravel/folio (FOLIO) - v1
|
||||
- laravel/framework (LARAVEL) - v11
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/pulse (PULSE) - v1
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- livewire/livewire (LIVEWIRE) - v4
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- pestphp/pest (PEST) - v2
|
||||
- phpunit/phpunit (PHPUNIT) - v10
|
||||
- laravel-echo (ECHO) - v1
|
||||
- tailwindcss (TAILWINDCSS) - v3
|
||||
|
||||
## Conventions
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail yarn run build`, `vendor/bin/sail yarn run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
## Replies
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
|
||||
## URLs
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||
- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
=== sail rules ===
|
||||
|
||||
## Laravel Sail
|
||||
|
||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
|
||||
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||
- Install Composer packages: `vendor/bin/sail composer install`
|
||||
- Execute Node commands: `vendor/bin/sail yarn run dev`
|
||||
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||
|
||||
=== folio/core rules ===
|
||||
|
||||
## Laravel Folio
|
||||
|
||||
- Laravel Folio is a file-based router. With Laravel Folio, a new route is created for every Blade file within the configured Folio directory. For example, pages are usually in `resources/views/pages/` and the file structure determines routes:
|
||||
- `pages/index.blade.php` → `/`
|
||||
- `pages/profile/index.blade.php` → `/profile`
|
||||
- `pages/auth/login.blade.php` → `/auth/login`
|
||||
- You may list available Folio routes using `vendor/bin/sail artisan folio:list` or using the `list-routes` tool.
|
||||
|
||||
### New Pages & Routes
|
||||
- Always create new `folio` pages and routes using `vendor/bin/sail artisan folio:page [name]` following existing naming conventions.
|
||||
|
||||
<code-snippet name="Example folio:page Commands for Automatic Routing" lang="shell">
|
||||
// Creates: resources/views/pages/products.blade.php → /products
|
||||
vendor/bin/sail artisan folio:page "products"
|
||||
|
||||
// Creates: resources/views/pages/products/[id].blade.php → /products/{id}
|
||||
vendor/bin/sail artisan folio:page "products/[id]"
|
||||
</code-snippet>
|
||||
|
||||
- Add a 'name' to each new Folio page at the very top of the file so it has a named route available for other parts of the codebase to use.
|
||||
|
||||
<code-snippet name="Adding Named Route to Folio Page" lang="php">
|
||||
use function Laravel\Folio\name;
|
||||
|
||||
name('products.index');
|
||||
</code-snippet>
|
||||
|
||||
### Support & Documentation
|
||||
- Folio supports: middleware, serving pages from multiple paths, subdomain routing, named routes, nested routes, index routes, route parameters, and route model binding.
|
||||
- If available, use the `search-docs` tool to use Folio to its full potential and help the user effectively.
|
||||
|
||||
<code-snippet name="Folio Middleware Example" lang="php">
|
||||
use function Laravel\Folio\{name, middleware};
|
||||
|
||||
name('admin.products');
|
||||
middleware(['auth', 'verified', 'can:manage-products']);
|
||||
?>
|
||||
</code-snippet>
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
## Do Things the Laravel Way
|
||||
|
||||
- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Database
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
### Controllers & Validation
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
### URL Generation
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
### Configuration
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
### Testing
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
### Vite Error
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail yarn run build` or ask the user to run `vendor/bin/sail yarn run dev` or `vendor/bin/sail composer run dev`.
|
||||
|
||||
=== laravel/v11 rules ===
|
||||
|
||||
## Laravel 11
|
||||
|
||||
- Use the `search-docs` tool to get version-specific documentation.
|
||||
- Laravel 11 brought a new streamlined file structure which this project now uses.
|
||||
|
||||
### Laravel 11 Structure
|
||||
- In Laravel 11, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
### Database
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
### New Artisan Commands
|
||||
- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11:
|
||||
- `vendor/bin/sail artisan make:enum`
|
||||
- `vendor/bin/sail artisan make:class`
|
||||
- `vendor/bin/sail artisan make:interface`
|
||||
|
||||
=== livewire/core rules ===
|
||||
|
||||
## Livewire
|
||||
|
||||
- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests.
|
||||
- Use the `vendor/bin/sail artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
||||
- State should live on the server, with the UI reflecting it.
|
||||
- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions.
|
||||
|
||||
## Livewire Best Practices
|
||||
- Livewire components require a single root element.
|
||||
- Use `wire:loading` and `wire:dirty` for delightful loading states.
|
||||
- Add `wire:key` in loops:
|
||||
|
||||
```blade
|
||||
@foreach ($items as $item)
|
||||
<div wire:key="item-{{ $item->id }}">
|
||||
{{ $item->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
|
||||
|
||||
<code-snippet name="Lifecycle Hook Examples" lang="php">
|
||||
public function mount(User $user) { $this->user = $user; }
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
</code-snippet>
|
||||
|
||||
## Testing Livewire
|
||||
|
||||
<code-snippet name="Example Livewire Component Test" lang="php">
|
||||
Livewire::test(Counter::class)
|
||||
->assertSet('count', 0)
|
||||
->call('increment')
|
||||
->assertSet('count', 1)
|
||||
->assertSee(1)
|
||||
->assertStatus(200);
|
||||
</code-snippet>
|
||||
|
||||
<code-snippet name="Testing Livewire Component Exists on Page" lang="php">
|
||||
$this->get('/posts/create')
|
||||
->assertSeeLivewire(CreatePost::class);
|
||||
</code-snippet>
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
## Laravel Pint Code Formatter
|
||||
|
||||
- You must run `vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/sail bin pint --test`, simply run `vendor/bin/sail bin pint` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
### Testing
|
||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||
|
||||
### Pest Tests
|
||||
- All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||
- Pest tests look and behave like this:
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||
- To run all tests: `vendor/bin/sail artisan test --compact`.
|
||||
- To run all tests in a file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `vendor/bin/sail artisan test --compact --filter=testName` (recommended after making a change to a related file).
|
||||
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
|
||||
|
||||
### Pest Assertions
|
||||
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
|
||||
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
|
||||
it('returns all', function () {
|
||||
$response = $this->postJson('/api/docs', []);
|
||||
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Mocking
|
||||
- Mocking can be very helpful when appropriate.
|
||||
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
|
||||
- You can also create partial mocks using the same import or self method.
|
||||
|
||||
### Datasets
|
||||
- Use datasets in Pest to simplify tests that have a lot of duplicated data. This is often the case when testing validation rules, so consider this solution when writing tests for validation rules.
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
</code-snippet>
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.).
|
||||
- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically.
|
||||
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
|
||||
|
||||
### Spacing
|
||||
- When listing items, use gap utilities for spacing; don't use margins.
|
||||
|
||||
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Superior</div>
|
||||
<div>Michigan</div>
|
||||
<div>Erie</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Dark Mode
|
||||
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
|
||||
|
||||
=== tailwindcss/v3 rules ===
|
||||
|
||||
## Tailwind CSS 3
|
||||
|
||||
- Always use Tailwind CSS v3; verify you're using only classes supported by this version.
|
||||
</laravel-boost-guidelines>
|
||||
375
CLAUDE.md
Normal file
375
CLAUDE.md
Normal file
@@ -0,0 +1,375 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3.29
|
||||
- laravel/folio (FOLIO) - v1
|
||||
- laravel/framework (LARAVEL) - v11
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/pulse (PULSE) - v1
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- livewire/livewire (LIVEWIRE) - v4
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- pestphp/pest (PEST) - v2
|
||||
- phpunit/phpunit (PHPUNIT) - v10
|
||||
- laravel-echo (ECHO) - v1
|
||||
- tailwindcss (TAILWINDCSS) - v3
|
||||
|
||||
## Conventions
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail yarn run build`, `vendor/bin/sail yarn run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
## Replies
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
|
||||
## URLs
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||
- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
=== sail rules ===
|
||||
|
||||
## Laravel Sail
|
||||
|
||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
|
||||
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||
- Install Composer packages: `vendor/bin/sail composer install`
|
||||
- Execute Node commands: `vendor/bin/sail yarn run dev`
|
||||
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||
|
||||
=== folio/core rules ===
|
||||
|
||||
## Laravel Folio
|
||||
|
||||
- Laravel Folio is a file-based router. With Laravel Folio, a new route is created for every Blade file within the configured Folio directory. For example, pages are usually in `resources/views/pages/` and the file structure determines routes:
|
||||
- `pages/index.blade.php` → `/`
|
||||
- `pages/profile/index.blade.php` → `/profile`
|
||||
- `pages/auth/login.blade.php` → `/auth/login`
|
||||
- You may list available Folio routes using `vendor/bin/sail artisan folio:list` or using the `list-routes` tool.
|
||||
|
||||
### New Pages & Routes
|
||||
- Always create new `folio` pages and routes using `vendor/bin/sail artisan folio:page [name]` following existing naming conventions.
|
||||
|
||||
<code-snippet name="Example folio:page Commands for Automatic Routing" lang="shell">
|
||||
// Creates: resources/views/pages/products.blade.php → /products
|
||||
vendor/bin/sail artisan folio:page "products"
|
||||
|
||||
// Creates: resources/views/pages/products/[id].blade.php → /products/{id}
|
||||
vendor/bin/sail artisan folio:page "products/[id]"
|
||||
</code-snippet>
|
||||
|
||||
- Add a 'name' to each new Folio page at the very top of the file so it has a named route available for other parts of the codebase to use.
|
||||
|
||||
<code-snippet name="Adding Named Route to Folio Page" lang="php">
|
||||
use function Laravel\Folio\name;
|
||||
|
||||
name('products.index');
|
||||
</code-snippet>
|
||||
|
||||
### Support & Documentation
|
||||
- Folio supports: middleware, serving pages from multiple paths, subdomain routing, named routes, nested routes, index routes, route parameters, and route model binding.
|
||||
- If available, use the `search-docs` tool to use Folio to its full potential and help the user effectively.
|
||||
|
||||
<code-snippet name="Folio Middleware Example" lang="php">
|
||||
use function Laravel\Folio\{name, middleware};
|
||||
|
||||
name('admin.products');
|
||||
middleware(['auth', 'verified', 'can:manage-products']);
|
||||
?>
|
||||
</code-snippet>
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
## Do Things the Laravel Way
|
||||
|
||||
- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Database
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
### Controllers & Validation
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
### URL Generation
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
### Configuration
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
### Testing
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
### Vite Error
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail yarn run build` or ask the user to run `vendor/bin/sail yarn run dev` or `vendor/bin/sail composer run dev`.
|
||||
|
||||
=== laravel/v11 rules ===
|
||||
|
||||
## Laravel 11
|
||||
|
||||
- Use the `search-docs` tool to get version-specific documentation.
|
||||
- Laravel 11 brought a new streamlined file structure which this project now uses.
|
||||
|
||||
### Laravel 11 Structure
|
||||
- In Laravel 11, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
### Database
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
### New Artisan Commands
|
||||
- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11:
|
||||
- `vendor/bin/sail artisan make:enum`
|
||||
- `vendor/bin/sail artisan make:class`
|
||||
- `vendor/bin/sail artisan make:interface`
|
||||
|
||||
=== livewire/core rules ===
|
||||
|
||||
## Livewire
|
||||
|
||||
- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests.
|
||||
- Use the `vendor/bin/sail artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
||||
- State should live on the server, with the UI reflecting it.
|
||||
- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions.
|
||||
|
||||
## Livewire Best Practices
|
||||
- Livewire components require a single root element.
|
||||
- Use `wire:loading` and `wire:dirty` for delightful loading states.
|
||||
- Add `wire:key` in loops:
|
||||
|
||||
```blade
|
||||
@foreach ($items as $item)
|
||||
<div wire:key="item-{{ $item->id }}">
|
||||
{{ $item->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
|
||||
|
||||
<code-snippet name="Lifecycle Hook Examples" lang="php">
|
||||
public function mount(User $user) { $this->user = $user; }
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
</code-snippet>
|
||||
|
||||
## Testing Livewire
|
||||
|
||||
<code-snippet name="Example Livewire Component Test" lang="php">
|
||||
Livewire::test(Counter::class)
|
||||
->assertSet('count', 0)
|
||||
->call('increment')
|
||||
->assertSet('count', 1)
|
||||
->assertSee(1)
|
||||
->assertStatus(200);
|
||||
</code-snippet>
|
||||
|
||||
<code-snippet name="Testing Livewire Component Exists on Page" lang="php">
|
||||
$this->get('/posts/create')
|
||||
->assertSeeLivewire(CreatePost::class);
|
||||
</code-snippet>
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
## Laravel Pint Code Formatter
|
||||
|
||||
- You must run `vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/sail bin pint --test`, simply run `vendor/bin/sail bin pint` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
### Testing
|
||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||
|
||||
### Pest Tests
|
||||
- All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||
- Pest tests look and behave like this:
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||
- To run all tests: `vendor/bin/sail artisan test --compact`.
|
||||
- To run all tests in a file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `vendor/bin/sail artisan test --compact --filter=testName` (recommended after making a change to a related file).
|
||||
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
|
||||
|
||||
### Pest Assertions
|
||||
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
|
||||
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
|
||||
it('returns all', function () {
|
||||
$response = $this->postJson('/api/docs', []);
|
||||
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Mocking
|
||||
- Mocking can be very helpful when appropriate.
|
||||
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
|
||||
- You can also create partial mocks using the same import or self method.
|
||||
|
||||
### Datasets
|
||||
- Use datasets in Pest to simplify tests that have a lot of duplicated data. This is often the case when testing validation rules, so consider this solution when writing tests for validation rules.
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
</code-snippet>
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.).
|
||||
- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically.
|
||||
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
|
||||
|
||||
### Spacing
|
||||
- When listing items, use gap utilities for spacing; don't use margins.
|
||||
|
||||
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Superior</div>
|
||||
<div>Michigan</div>
|
||||
<div>Erie</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Dark Mode
|
||||
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
|
||||
|
||||
=== tailwindcss/v3 rules ===
|
||||
|
||||
## Tailwind CSS 3
|
||||
|
||||
- Always use Tailwind CSS v3; verify you're using only classes supported by this version.
|
||||
</laravel-boost-guidelines>
|
||||
171
app/Livewire/Association/Election/Admin.php
Normal file
171
app/Livewire/Association/Election/Admin.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\Election;
|
||||
|
||||
use App\Models\Election;
|
||||
use Livewire\Component;
|
||||
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;
|
||||
|
||||
final class Admin extends Component
|
||||
{
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?array $votes = null;
|
||||
|
||||
public ?array $boardVotes = null;
|
||||
|
||||
public ?array $events = null;
|
||||
|
||||
public ?array $boardEvents = null;
|
||||
|
||||
public ?Election $election = null;
|
||||
|
||||
public string $signThisEvent = '';
|
||||
|
||||
public array $plebs = [];
|
||||
|
||||
public array $electionConfig = [];
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'echo:votes,.newVote' => 'handleNewVote',
|
||||
];
|
||||
|
||||
public function mount(Election $election): void
|
||||
{
|
||||
$this->election = $election;
|
||||
$this->loadEvents();
|
||||
$this->loadBoardEvents();
|
||||
$this->loadVotes();
|
||||
$this->loadBoardVotes();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
$this->currentPubkey = $pubkey;
|
||||
$allowedPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
dd($this->isAllowed);
|
||||
}
|
||||
|
||||
public function handleNewVote(): void
|
||||
{
|
||||
$this->loadEvents();
|
||||
$this->loadBoardEvents();
|
||||
$this->loadVotes();
|
||||
$this->loadBoardVotes();
|
||||
}
|
||||
|
||||
public function loadVotes(): void
|
||||
{
|
||||
$this->votes = collect($this->events)
|
||||
->map(fn ($event) => [
|
||||
'created_at' => $event['created_at'],
|
||||
'pubkey' => $event['pubkey'],
|
||||
'forpubkey' => $this->fetchProfile($event['content']),
|
||||
'type' => str($event['content'])->after(',')->toString(),
|
||||
])
|
||||
->sortByDesc('created_at')
|
||||
->unique(fn ($event) => $event['pubkey'].$event['type'])
|
||||
->values()
|
||||
->groupBy('type')
|
||||
->map(fn ($votes) => [
|
||||
'type' => $votes[0]['type'],
|
||||
'votes' => $votes->groupBy('forpubkey')->map(fn ($group) => ['count' => $group->count()])->toArray(),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function loadBoardVotes(): void
|
||||
{
|
||||
$this->boardVotes = collect($this->boardEvents)
|
||||
->map(fn ($event) => [
|
||||
'created_at' => $event['created_at'],
|
||||
'pubkey' => $event['pubkey'],
|
||||
'forpubkey' => $this->fetchProfile($event['content']),
|
||||
'type' => str($event['content'])->after(',')->toString(),
|
||||
])
|
||||
->sortByDesc('created_at')
|
||||
->values()
|
||||
->groupBy('type')
|
||||
->map(fn ($votes) => [
|
||||
'type' => $votes[0]['type'],
|
||||
'votes' => $votes->groupBy('forpubkey')->map(fn ($group) => ['count' => $group->count()])->toArray(),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function loadEvents(): void
|
||||
{
|
||||
$this->events = $this->loadNostrEvents([32122]);
|
||||
}
|
||||
|
||||
public function loadBoardEvents(): void
|
||||
{
|
||||
$this->boardEvents = $this->loadNostrEvents([2121]);
|
||||
}
|
||||
|
||||
public function fetchProfile($content): string
|
||||
{
|
||||
$pubkey = str($content)->before(',')->toString();
|
||||
$profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first();
|
||||
if (! $profile) {
|
||||
\Artisan::call(\App\Console\Commands\Nostr\FetchProfile::class, ['--pubkey' => $pubkey]);
|
||||
$profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first();
|
||||
}
|
||||
|
||||
return $profile->pubkey;
|
||||
}
|
||||
|
||||
public function loadNostrEvents($kinds): array
|
||||
{
|
||||
$subscription = new Subscription;
|
||||
$subscriptionId = $subscription->setId();
|
||||
$filter = new Filter;
|
||||
$filter->setKinds($kinds);
|
||||
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
||||
$relaySet = new RelaySet;
|
||||
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
||||
$request = new Request($relaySet, $requestMessage);
|
||||
$response = $request->send();
|
||||
|
||||
return collect($response[config('services.relay')])
|
||||
->map(function ($event) {
|
||||
if (! isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
71
app/Livewire/Association/Election/Index.php
Normal file
71
app/Livewire/Association/Election/Index.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\Election;
|
||||
|
||||
use App\Models\EinundzwanzigPleb;
|
||||
use App\Models\Election;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
|
||||
final class Index extends Component
|
||||
{
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
public array $elections = [];
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->elections = Election::query()
|
||||
->get()
|
||||
->toArray();
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$logPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->where('pubkey', $pubkey)->first();
|
||||
$logPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveElection($index): void
|
||||
{
|
||||
$election = $this->elections[$index];
|
||||
$electionModel = Election::find($election['id']);
|
||||
$electionModel->candidates = $election['candidates'];
|
||||
$electionModel->save();
|
||||
}
|
||||
}
|
||||
186
app/Livewire/Association/Election/Show.php
Normal file
186
app/Livewire/Association/Election/Show.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\Election;
|
||||
|
||||
use App\Models\EinundzwanzigPleb;
|
||||
use App\Models\Election;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
use swentel\nostr\Event\Event as NostrEvent;
|
||||
use swentel\nostr\Filter\Filter;
|
||||
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;
|
||||
|
||||
final class Show extends Component
|
||||
{
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public bool $showLog = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
public array $events = [];
|
||||
|
||||
public array $boardEvents = [];
|
||||
|
||||
public Election $election;
|
||||
|
||||
public array $plebs = [];
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public string $signThisEvent = '';
|
||||
|
||||
public bool $isNotClosed = true;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
'echo:votes,.newVote' => 'handleNewVote',
|
||||
];
|
||||
|
||||
public function mount(Election $election): void
|
||||
{
|
||||
$this->election = $election;
|
||||
$this->plebs = EinundzwanzigPleb::query()
|
||||
->with(['profile'])
|
||||
->whereIn('association_status', [3, 4])
|
||||
->orderBy('association_status', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
$this->loadEvents();
|
||||
$this->loadBoardEvents();
|
||||
if ($this->election->end_time?->isPast() || ! config('services.voting')) {
|
||||
$this->isNotClosed = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSearch($value): void
|
||||
{
|
||||
$this->plebs = EinundzwanzigPleb::query()
|
||||
->with(['profile'])
|
||||
->whereIn('association_status', [3, 4])
|
||||
->where(fn ($query) => $query
|
||||
->where('pubkey', 'like', "%$value%")
|
||||
->orWhereHas('profile', fn ($query) => $query->where('name', 'ilike', "%$value%")))
|
||||
->orderBy('association_status', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$logPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||
$this->showLog = true;
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function handleNewVote(): void
|
||||
{
|
||||
$this->loadEvents();
|
||||
$this->loadBoardEvents();
|
||||
}
|
||||
|
||||
public function loadEvents(): void
|
||||
{
|
||||
$this->events = $this->loadNostrEvents([32122]);
|
||||
}
|
||||
|
||||
public function loadBoardEvents(): void
|
||||
{
|
||||
$this->boardEvents = $this->loadNostrEvents([2121]);
|
||||
}
|
||||
|
||||
public function loadNostrEvents($kinds): array
|
||||
{
|
||||
$subscription = new Subscription;
|
||||
$subscriptionId = $subscription->setId();
|
||||
$filter = new Filter;
|
||||
$filter->setKinds($kinds);
|
||||
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
||||
$relaySet = new RelaySet;
|
||||
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
||||
$request = new Request($relaySet, $requestMessage);
|
||||
$response = $request->send();
|
||||
|
||||
return collect($response[config('services.relay')])
|
||||
->map(function ($event) {
|
||||
if (! isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function vote($pubkey, $type, $board = false): void
|
||||
{
|
||||
if ($this->election->end_time?->isPast()) {
|
||||
$this->isNotClosed = false;
|
||||
|
||||
return;
|
||||
}
|
||||
$note = new NostrEvent;
|
||||
$note->setKind($board ? 2121 : 32122);
|
||||
if (! $board) {
|
||||
$dTag = sprintf('%s,%s,%s', $this->currentPleb->pubkey, date('Y'), $type);
|
||||
$note->setTags([['d', $dTag]]);
|
||||
}
|
||||
$note->setContent("$pubkey,$type");
|
||||
$this->signThisEvent = $note->toJson();
|
||||
}
|
||||
|
||||
public function checkElection(): void
|
||||
{
|
||||
if ($this->election->end_time?->isPast()) {
|
||||
$this->isNotClosed = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function signEvent($event): void
|
||||
{
|
||||
$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);
|
||||
$relay = new Relay(config('services.relay'));
|
||||
$relay->setMessage($eventMessage);
|
||||
$relay->send();
|
||||
\App\Support\Broadcast::on('votes')->as('newVote')->sendNow();
|
||||
}
|
||||
}
|
||||
66
app/Livewire/Association/Members/Admin.php
Normal file
66
app/Livewire/Association/Members/Admin.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\Members;
|
||||
|
||||
use App\Models\EinundzwanzigPleb;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
|
||||
final class Admin extends Component
|
||||
{
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->where('pubkey', $this->currentPubkey)->first();
|
||||
$allowedPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
'7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d',
|
||||
'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
|
||||
'19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3',
|
||||
'acbcec475a1a4f9481939ecfbd1c3d111f5b5a474a39ae039bbc720fdd305bec',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->where('pubkey', $pubkey)->first();
|
||||
$allowedPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
'7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d',
|
||||
'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
|
||||
'19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3',
|
||||
'acbcec475a1a4f9481939ecfbd1c3d111f5b5a474a39ae039bbc720fdd305bec',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
app/Livewire/Association/News/Index.php
Normal file
122
app/Livewire/Association/News/Index.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\News;
|
||||
|
||||
use App\Livewire\Forms\NotificationForm;
|
||||
use App\Models\EinundzwanzigPleb;
|
||||
use App\Models\Notification;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use WireUi\Actions\Notification as WireNotification;
|
||||
|
||||
final class Index extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public NotificationForm $form;
|
||||
|
||||
public ?\Illuminate\Http\UploadedFile $file = null;
|
||||
|
||||
public \Illuminate\Database\Eloquent\Collection $news;
|
||||
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public bool $canEdit = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
if (in_array($this->currentPleb->npub, config('einundzwanzig.config.current_board'), true)) {
|
||||
$this->canEdit = true;
|
||||
}
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
$this->refreshNews();
|
||||
}
|
||||
|
||||
public function refreshNews(): void
|
||||
{
|
||||
$this->news = Notification::query()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
if (in_array($this->currentPleb->npub, config('einundzwanzig.config.current_board'), true)) {
|
||||
$this->canEdit = true;
|
||||
}
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->form->validate();
|
||||
|
||||
$this->validate([
|
||||
'file' => 'required|file|mimes:pdf|max:1024',
|
||||
]);
|
||||
|
||||
$notification = Notification::query()
|
||||
->orderBy('created_at', 'desc')
|
||||
->create([
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
'category' => $this->form->category,
|
||||
'name' => $this->form->name,
|
||||
'description' => $this->form->description,
|
||||
]);
|
||||
|
||||
$notification
|
||||
->addMedia($this->file->getRealPath())
|
||||
->usingName($this->file->getClientOriginalName())
|
||||
->toMediaCollection('pdf');
|
||||
|
||||
$this->form->reset();
|
||||
$this->file = null;
|
||||
|
||||
$this->refreshNews();
|
||||
}
|
||||
|
||||
public function delete($id): void
|
||||
{
|
||||
$notification = new WireNotification($this);
|
||||
$notification->confirm([
|
||||
'title' => 'Post löschen',
|
||||
'message' => 'Bist du sicher, dass du diesen Post löschen möchtest?',
|
||||
'accept' => [
|
||||
'label' => 'Ja, löschen',
|
||||
'method' => 'deleteNow',
|
||||
'params' => $id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteNow($id): void
|
||||
{
|
||||
$notification = Notification::query()->find($id);
|
||||
$notification->delete();
|
||||
$this->refreshNews();
|
||||
}
|
||||
}
|
||||
304
app/Livewire/Association/Profile.php
Normal file
304
app/Livewire/Association/Profile.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association;
|
||||
|
||||
use App\Enums\AssociationStatus;
|
||||
use App\Livewire\Forms\ApplicationForm;
|
||||
use App\Models\EinundzwanzigPleb;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
use swentel\nostr\Event\Event as NostrEvent;
|
||||
use swentel\nostr\Filter\Filter;
|
||||
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\Sign\Sign;
|
||||
use swentel\nostr\Subscription\Subscription;
|
||||
use WireUi\Actions\Notification;
|
||||
|
||||
final class Profile extends Component
|
||||
{
|
||||
public ApplicationForm $form;
|
||||
|
||||
public bool $no = false;
|
||||
|
||||
public bool $showEmail = true;
|
||||
|
||||
public string $fax = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public array $yearsPaid = [];
|
||||
|
||||
public array $events = [];
|
||||
|
||||
public \Illuminate\Database\Eloquent\Collection $payments;
|
||||
|
||||
public int $amountToPay;
|
||||
|
||||
public bool $currentYearIsPaid = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = EinundzwanzigPleb::query()
|
||||
->with([
|
||||
'paymentEvents' => fn ($query) => $query->where('year', date('Y')),
|
||||
])
|
||||
->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->email = $this->currentPleb->email;
|
||||
$this->no = $this->currentPleb->no_email;
|
||||
$this->showEmail = ! $this->no;
|
||||
if ($this->currentPleb->association_status === AssociationStatus::ACTIVE) {
|
||||
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
|
||||
}
|
||||
if ($this->currentPleb->paymentEvents->count() < 1) {
|
||||
$this->createPaymentEvent();
|
||||
$this->currentPleb->load('paymentEvents');
|
||||
}
|
||||
$this->loadEvents();
|
||||
$this->listenForPayment();
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = EinundzwanzigPleb::query()
|
||||
->with([
|
||||
'paymentEvents' => fn ($query) => $query->where('year', date('Y')),
|
||||
])
|
||||
->where('pubkey', $pubkey)->first();
|
||||
$this->email = $this->currentPleb->email;
|
||||
$this->no = $this->currentPleb->no_email;
|
||||
$this->showEmail = ! $this->no;
|
||||
if ($this->currentPleb->association_status === AssociationStatus::ACTIVE) {
|
||||
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
|
||||
}
|
||||
if ($this->currentPleb->paymentEvents->count() < 1) {
|
||||
$this->createPaymentEvent();
|
||||
$this->currentPleb->load('paymentEvents');
|
||||
}
|
||||
$this->loadEvents();
|
||||
$this->listenForPayment();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
NostrAuth::logout();
|
||||
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
$this->yearsPaid = [];
|
||||
$this->events = [];
|
||||
$this->payments = [];
|
||||
$this->qrCode = null;
|
||||
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
|
||||
$this->currentYearIsPaid = false;
|
||||
}
|
||||
|
||||
public function updatedNo(): void
|
||||
{
|
||||
$this->showEmail = ! $this->no;
|
||||
$this->currentPleb->update([
|
||||
'no_email' => $this->no,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatedFax(): void
|
||||
{
|
||||
$this->js('alert("Markus Turm wird sich per Fax melden!")');
|
||||
}
|
||||
|
||||
public function saveEmail(): void
|
||||
{
|
||||
$this->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
$this->currentPleb->update([
|
||||
'email' => $this->email,
|
||||
]);
|
||||
$notification = new Notification($this);
|
||||
$notification->success('E-Mail Adresse gespeichert.');
|
||||
}
|
||||
|
||||
public function pay($comment): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$paymentEvent = $this->currentPleb
|
||||
->paymentEvents()
|
||||
->where('year', date('Y'))
|
||||
->first();
|
||||
if ($paymentEvent->btc_pay_invoice) {
|
||||
return redirect('https://pay.einundzwanzig.space/i/'.$paymentEvent->btc_pay_invoice);
|
||||
}
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
'Authorization' => 'token '.config('services.btc_pay.api_key'),
|
||||
])->post(
|
||||
'https://pay.einundzwanzig.space/api/v1/stores/98PF86BoMd3C8P1nHHyFdoeznCwtcm5yehcAgoCYDQ2a/invoices',
|
||||
[
|
||||
'amount' => $this->amountToPay,
|
||||
'metadata' => [
|
||||
'orderId' => $comment,
|
||||
'orderUrl' => url()->route('association.profile'),
|
||||
'itemDesc' => 'Mitgliedsbeitrag '.date('Y').' von nostr:'.$this->currentPleb->npub,
|
||||
'posData' => [
|
||||
'event' => $paymentEvent->event_id,
|
||||
'pubkey' => $this->currentPleb->pubkey,
|
||||
'npub' => $this->currentPleb->npub,
|
||||
],
|
||||
],
|
||||
'checkout' => [
|
||||
'expirationMinutes' => 60 * 24,
|
||||
'redirectURL' => url()->route('association.profile'),
|
||||
'redirectAutomatically' => true,
|
||||
'defaultLanguage' => 'de',
|
||||
],
|
||||
],
|
||||
)->throw();
|
||||
$paymentEvent->btc_pay_invoice = $response->json()['id'];
|
||||
$paymentEvent->save();
|
||||
|
||||
return redirect($response->json()['checkoutLink']);
|
||||
} catch (\Exception $e) {
|
||||
$notification = new Notification($this);
|
||||
$notification->error(
|
||||
'Fehler beim Erstellen der Rechnung. Bitte versuche es später erneut: '.$e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function listenForPayment(): void
|
||||
{
|
||||
$paymentEvent = $this->currentPleb
|
||||
->paymentEvents()
|
||||
->where('year', date('Y'))
|
||||
->first();
|
||||
if ($paymentEvent->btc_pay_invoice) {
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
'Authorization' => 'token '.config('services.btc_pay.api_key'),
|
||||
])
|
||||
->get(
|
||||
'https://pay.einundzwanzig.space/api/v1/stores/98PF86BoMd3C8P1nHHyFdoeznCwtcm5yehcAgoCYDQ2a/invoices/'.$paymentEvent->btc_pay_invoice,
|
||||
);
|
||||
if ($response->json()['status'] === 'Expired') {
|
||||
$paymentEvent->btc_pay_invoice = null;
|
||||
$paymentEvent->paid = false;
|
||||
$paymentEvent->save();
|
||||
}
|
||||
if ($response->json()['status'] === 'Settled') {
|
||||
$paymentEvent->paid = true;
|
||||
$paymentEvent->save();
|
||||
$this->currentYearIsPaid = true;
|
||||
}
|
||||
}
|
||||
if ($paymentEvent->paid) {
|
||||
$this->currentYearIsPaid = true;
|
||||
}
|
||||
$paymentEvent = $paymentEvent->refresh();
|
||||
$this->payments = $this->currentPleb
|
||||
->paymentEvents()
|
||||
->where('paid', true)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function save($type): void
|
||||
{
|
||||
$this->form->validate();
|
||||
if (! $this->form->check) {
|
||||
$this->js('alert("Du musst den Statuten zustimmen.")');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->currentPleb
|
||||
->update([
|
||||
'association_status' => $type,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createPaymentEvent(): void
|
||||
{
|
||||
$note = new NostrEvent;
|
||||
$note->setKind(32121);
|
||||
$note->setContent(
|
||||
'Dieses Event dient der Zahlung des Mitgliedsbeitrags für das Jahr '.date(
|
||||
'Y',
|
||||
).'. Bitte bezahle den Betrag von '.number_format($this->amountToPay, 0, ',', '.').' Satoshis.',
|
||||
);
|
||||
$note->setTags([
|
||||
['d', $this->currentPleb->pubkey.','.date('Y')],
|
||||
['zap', 'daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6', config('services.relay'), '1'],
|
||||
]);
|
||||
$signer = new Sign;
|
||||
$signer->signEvent($note, config('services.nostr'));
|
||||
|
||||
$eventMessage = new EventMessage($note);
|
||||
|
||||
$relayUrl = config('services.relay');
|
||||
$relay = new Relay($relayUrl);
|
||||
$relay->setMessage($eventMessage);
|
||||
$result = $relay->send();
|
||||
|
||||
$this->currentPleb->paymentEvents()->create([
|
||||
'year' => date('Y'),
|
||||
'event_id' => $result->eventId,
|
||||
'amount' => $this->amountToPay,
|
||||
]);
|
||||
}
|
||||
|
||||
public function loadEvents(): void
|
||||
{
|
||||
$subscription = new Subscription;
|
||||
$subscriptionId = $subscription->setId();
|
||||
|
||||
$filter1 = new Filter;
|
||||
$filter1->setKinds([32121]);
|
||||
$filter1->setAuthors(['daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6']);
|
||||
$filters = [$filter1];
|
||||
|
||||
$requestMessage = new RequestMessage($subscriptionId, $filters);
|
||||
|
||||
$relays = [
|
||||
new Relay(config('services.relay')),
|
||||
];
|
||||
$relaySet = new RelaySet;
|
||||
$relaySet->setRelays($relays);
|
||||
|
||||
$request = new Request($relaySet, $requestMessage);
|
||||
$response = $request->send();
|
||||
|
||||
$this->events = collect($response[config('services.relay')])
|
||||
->map(function ($event) {
|
||||
if (! isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->unique('id')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
70
app/Livewire/Association/ProjectSupport/Form/Create.php
Normal file
70
app/Livewire/Association/ProjectSupport/Form/Create.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\ProjectSupport\Form;
|
||||
|
||||
use App\Livewire\Forms\ProjectProposalForm;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
final class Create extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public ProjectProposalForm $form;
|
||||
|
||||
public ?\Illuminate\Http\UploadedFile $image = null;
|
||||
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function save(): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$this->form->validate();
|
||||
|
||||
$projectProposal = \App\Models\ProjectProposal::query()->create([
|
||||
...$this->form->all(),
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
]);
|
||||
if ($this->image) {
|
||||
$this->validate([
|
||||
'image' => 'image|max:1024',
|
||||
]);
|
||||
$projectProposal
|
||||
->addMedia($this->image->getRealPath())
|
||||
->toMediaCollection('main');
|
||||
}
|
||||
|
||||
return redirect()->route('association.projectSupport');
|
||||
}
|
||||
}
|
||||
75
app/Livewire/Association/ProjectSupport/Form/Edit.php
Normal file
75
app/Livewire/Association/ProjectSupport/Form/Edit.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\ProjectSupport\Form;
|
||||
|
||||
use App\Livewire\Forms\ProjectProposalForm;
|
||||
use App\Models\ProjectProposal;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
final class Edit extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public ProjectProposalForm $form;
|
||||
|
||||
public ?ProjectProposal $projectProposal = null;
|
||||
|
||||
public ?\Illuminate\Http\UploadedFile $image = null;
|
||||
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
];
|
||||
|
||||
public function mount(ProjectProposal $projectProposal): void
|
||||
{
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
$this->form->fill($projectProposal->toArray());
|
||||
$this->projectProposal = $projectProposal;
|
||||
$this->image = $projectProposal->getFirstMedia('main');
|
||||
}
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function save(): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$this->form->validate();
|
||||
if ($this->image && method_exists($this->image, 'temporaryUrl')) {
|
||||
$this->validate([
|
||||
'image' => 'nullable|image|max:1024',
|
||||
]);
|
||||
$this->projectProposal
|
||||
->addMedia($this->image->getRealPath())
|
||||
->toMediaCollection('main');
|
||||
}
|
||||
|
||||
$this->projectProposal->update([
|
||||
...$this->form->except('id', 'slug'),
|
||||
]);
|
||||
|
||||
return redirect()->route('association.projectSupport');
|
||||
}
|
||||
}
|
||||
102
app/Livewire/Association/ProjectSupport/Index.php
Normal file
102
app/Livewire/Association/ProjectSupport/Index.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\ProjectSupport;
|
||||
|
||||
use App\Models\ProjectProposal;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
use WireUi\Actions\Notification;
|
||||
|
||||
final class Index extends Component
|
||||
{
|
||||
public string $activeFilter = 'all';
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public \Illuminate\Database\Eloquent\Collection $projects;
|
||||
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?\App\Models\EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadProjects();
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->loadProjects();
|
||||
}
|
||||
|
||||
public function loadProjects(): void
|
||||
{
|
||||
$this->projects = ProjectProposal::query()
|
||||
->with([
|
||||
'einundzwanzigPleb.profile',
|
||||
'votes',
|
||||
])
|
||||
->where(function ($query) {
|
||||
$query
|
||||
->where('name', 'ilike', '%'.$this->search.'%')
|
||||
->orWhere('description', 'ilike', '%'.$this->search.'%')
|
||||
->orWhereHas('einundzwanzigPleb.profile', function ($q) {
|
||||
$q->where('name', 'ilike', '%'.$this->search.'%');
|
||||
});
|
||||
})
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function confirmDelete($id): void
|
||||
{
|
||||
$notification = new Notification($this);
|
||||
$notification->confirm([
|
||||
'title' => 'Projektunterstützung löschen',
|
||||
'message' => 'Bist du sicher, dass du diese Projektunterstützung löschen möchtest?',
|
||||
'accept' => [
|
||||
'label' => 'Ja, löschen',
|
||||
'method' => 'delete',
|
||||
'params' => $id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function setFilter($filter): void
|
||||
{
|
||||
$this->activeFilter = $filter;
|
||||
}
|
||||
|
||||
public function delete($id): void
|
||||
{
|
||||
ProjectProposal::query()->findOrFail($id)->delete();
|
||||
$this->loadProjects();
|
||||
}
|
||||
}
|
||||
109
app/Livewire/Association/ProjectSupport/Show.php
Normal file
109
app/Livewire/Association/ProjectSupport/Show.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Association\ProjectSupport;
|
||||
|
||||
use App\Livewire\Forms\VoteForm;
|
||||
use App\Models\ProjectProposal;
|
||||
use App\Models\Vote;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Component;
|
||||
|
||||
final class Show extends Component
|
||||
{
|
||||
public VoteForm $form;
|
||||
|
||||
public ?ProjectProposal $projectProposal = null;
|
||||
|
||||
public bool $isAllowed = false;
|
||||
|
||||
public ?string $currentPubkey = null;
|
||||
|
||||
public ?\App\Models\EinundzwanzigPleb $currentPleb = null;
|
||||
|
||||
public bool $ownVoteExists = false;
|
||||
|
||||
public \Illuminate\Database\Eloquent\Collection $boardVotes;
|
||||
|
||||
public \Illuminate\Database\Eloquent\Collection $otherVotes;
|
||||
|
||||
protected $listeners = [
|
||||
'nostrLoggedIn' => 'handleNostrLoggedIn',
|
||||
'nostrLoggedOut' => 'handleNostrLoggedOut',
|
||||
];
|
||||
|
||||
public function mount(ProjectProposal $projectProposal): void
|
||||
{
|
||||
$this->projectProposal = $projectProposal;
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->handleNostrLoggedIn($this->currentPubkey);
|
||||
}
|
||||
$this->boardVotes = $this->getBoardVotes();
|
||||
$this->otherVotes = $this->getOtherVotes();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($pubkey): void
|
||||
{
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
$this->ownVoteExists = Vote::query()
|
||||
->where('project_proposal_id', $this->projectProposal->id)
|
||||
->where('einundzwanzig_pleb_id', $this->currentPleb->id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function handleNostrLoggedOut(): void
|
||||
{
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
}
|
||||
|
||||
public function getBoardVotes(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return Vote::query()
|
||||
->where('project_proposal_id', $this->projectProposal->id)
|
||||
->whereHas('einundzwanzigPleb', fn ($q) => $q->whereIn('npub', config('einundzwanzig.config.current_board')))
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getOtherVotes(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return Vote::query()
|
||||
->where('project_proposal_id', $this->projectProposal->id)
|
||||
->whereDoesntHave(
|
||||
'einundzwanzigPleb',
|
||||
fn ($q) => $q->whereIn('npub', config('einundzwanzig.config.current_board'))
|
||||
)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function approve(): void
|
||||
{
|
||||
Vote::query()->updateOrCreate([
|
||||
'project_proposal_id' => $this->projectProposal->id,
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
], [
|
||||
'value' => true,
|
||||
]);
|
||||
$this->form->reset();
|
||||
$this->ownVoteExists = true;
|
||||
$this->boardVotes = $this->getBoardVotes();
|
||||
$this->otherVotes = $this->getOtherVotes();
|
||||
}
|
||||
|
||||
public function notApprove(): void
|
||||
{
|
||||
$this->form->validate();
|
||||
|
||||
Vote::query()->updateOrCreate([
|
||||
'project_proposal_id' => $this->projectProposal->id,
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
], [
|
||||
'value' => false,
|
||||
]);
|
||||
$this->form->reset();
|
||||
$this->ownVoteExists = true;
|
||||
}
|
||||
}
|
||||
28
app/Livewire/Changelog.php
Normal file
28
app/Livewire/Changelog.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
final class Changelog extends Component
|
||||
{
|
||||
public array $entries = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$output = shell_exec('git log -n1000 --pretty=format:"%H|%s|%an|%ad" --date=format:"%Y-%m-%d %H:%M:%S"');
|
||||
$lines = explode("\n", trim($output));
|
||||
$entries = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
[$hash, $message, $author, $date] = explode('|', $line);
|
||||
$entries[] = [
|
||||
'hash' => $hash,
|
||||
'message' => $message,
|
||||
'author' => $author,
|
||||
'date' => $date,
|
||||
];
|
||||
}
|
||||
$this->entries = $entries;
|
||||
}
|
||||
}
|
||||
51
app/Livewire/EinundzwanzigFeed/Index.php
Normal file
51
app/Livewire/EinundzwanzigFeed/Index.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\EinundzwanzigFeed;
|
||||
|
||||
use App\Models\Event;
|
||||
use Livewire\Component;
|
||||
|
||||
final class Index extends Component
|
||||
{
|
||||
public array $events = [];
|
||||
|
||||
public bool $newEvents = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->events = Event::query()
|
||||
->where('type', 'root')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with([
|
||||
'renderedEvent',
|
||||
])
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function hydrate(): void
|
||||
{
|
||||
if ($this->newEvents) {
|
||||
$this->loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
#[Rule('echo:events,.newEvents')]
|
||||
public function updated(): void
|
||||
{
|
||||
$this->newEvents = true;
|
||||
}
|
||||
|
||||
public function loadMore(): void
|
||||
{
|
||||
$this->newEvents = false;
|
||||
$this->events = Event::query()
|
||||
->where('type', 'root')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with([
|
||||
'renderedEvent',
|
||||
])
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
10
app/Livewire/Meetups/Grid.php
Normal file
10
app/Livewire/Meetups/Grid.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Meetups;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
final class Grid extends Component
|
||||
{
|
||||
//
|
||||
}
|
||||
102
app/Livewire/Meetups/Mockup.php
Normal file
102
app/Livewire/Meetups/Mockup.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Meetups;
|
||||
|
||||
use Livewire\Component;
|
||||
use swentel\nostr\Event\Event as NostrEvent;
|
||||
use swentel\nostr\Filter\Filter;
|
||||
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;
|
||||
|
||||
final class Mockup extends Component
|
||||
{
|
||||
public array $events = [];
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public string $description = '';
|
||||
|
||||
public string $signThisEvent = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadEvents();
|
||||
}
|
||||
|
||||
public function loadEvents(): void
|
||||
{
|
||||
$subscription = new Subscription;
|
||||
$subscriptionId = $subscription->setId();
|
||||
|
||||
$filter1 = new Filter;
|
||||
$filter1->setKinds([31924]);
|
||||
$filter1->setLimit(25);
|
||||
$filters = [$filter1];
|
||||
|
||||
$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(function ($event) {
|
||||
if (! isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$note = new NostrEvent;
|
||||
$note->setContent($this->description);
|
||||
$note->setKind(31924);
|
||||
$note->setTags([
|
||||
['d', str()->uuid()->toString()],
|
||||
['title', $this->title],
|
||||
]);
|
||||
$this->signThisEvent = $note->toJson();
|
||||
}
|
||||
|
||||
public function signEvent($event): void
|
||||
{
|
||||
$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);
|
||||
$relay->send();
|
||||
|
||||
$this->title = '';
|
||||
$this->description = '';
|
||||
$this->loadEvents();
|
||||
}
|
||||
}
|
||||
10
app/Livewire/Meetups/Table.php
Normal file
10
app/Livewire/Meetups/Table.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Meetups;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
final class Table extends Component
|
||||
{
|
||||
//
|
||||
}
|
||||
20
app/Livewire/Meetups/Worldmap.php
Normal file
20
app/Livewire/Meetups/Worldmap.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Meetups;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
final class Worldmap extends Component
|
||||
{
|
||||
public array $markers = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->markers = [];
|
||||
}
|
||||
|
||||
public function filterByMarker($id): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
10
app/Livewire/Welcome.php
Normal file
10
app/Livewire/Welcome.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
final class Welcome extends Component
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
class VoltServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Volt::mount([
|
||||
config('livewire.view_path', resource_path('views/livewire')),
|
||||
resource_path('views/pages'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
14
boost.json
Normal file
14
boost.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"agents": [
|
||||
"claude_code",
|
||||
"opencode",
|
||||
"phpstorm"
|
||||
],
|
||||
"editors": [
|
||||
"claude_code",
|
||||
"opencode",
|
||||
"phpstorm"
|
||||
],
|
||||
"guidelines": [],
|
||||
"sail": true
|
||||
}
|
||||
@@ -4,5 +4,4 @@ return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FolioServiceProvider::class,
|
||||
App\Providers\NostrAuthServiceProvider::class,
|
||||
App\Providers\VoltServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
"laravel/sail": "^1.31",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.5",
|
||||
"livewire/volt": "^1.6",
|
||||
"livewire/livewire": "^4.0",
|
||||
"openspout/openspout": "^4.24",
|
||||
"power-components/livewire-powergrid": "^5.10",
|
||||
"power-components/livewire-powergrid": "^6.7",
|
||||
"ralphjsmit/laravel-seo": "^1.6",
|
||||
"sentry/sentry-laravel": "^4.9",
|
||||
"simplesoftwareio/simple-qrcode": "^4.2",
|
||||
@@ -40,6 +39,8 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/boost": "^1.8",
|
||||
"laravel/pail": "^1.2",
|
||||
"laravel/pint": "^1.13",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
@@ -64,7 +65,8 @@
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||
"@php artisan boost:update --ansi"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
@@ -73,6 +75,10 @@
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#c4b5fd,#fb7185,#fdba74\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"yarn run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
||||
1889
composer.lock
generated
1889
composer.lock
generated
File diff suppressed because it is too large
Load Diff
282
config/livewire.php
Normal file
282
config/livewire.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Component Locations
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value sets the root directories that'll be used to resolve view-based
|
||||
| components like single and multi-file components. The make command will
|
||||
| use the first directory in this array to add new component files to.
|
||||
|
|
||||
*/
|
||||
|
||||
'component_locations' => [
|
||||
resource_path('views/livewire'),
|
||||
resource_path('views/components'),
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Component Namespaces
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value sets default namespaces that will be used to resolve view-based
|
||||
| components like single-file and multi-file components. These folders'll
|
||||
| also be referenced when creating new components via the make command.
|
||||
|
|
||||
*/
|
||||
|
||||
'component_namespaces' => [
|
||||
'layouts' => resource_path('views/layouts'),
|
||||
'pages' => resource_path('views/pages'),
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Page Layout
|
||||
|---------------------------------------------------------------------------
|
||||
| The view that will be used as the layout when rendering a single component as
|
||||
| an entire page via `Route::livewire('/post/create', 'pages::create-post')`.
|
||||
| In this case, the content of pages::create-post will render into $slot.
|
||||
|
|
||||
*/
|
||||
|
||||
'component_layout' => 'layouts::app',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Lazy Loading Placeholder
|
||||
|---------------------------------------------------------------------------
|
||||
| Livewire allows you to lazy load components that would otherwise slow down
|
||||
| the initial page load. Every component can have a custom placeholder or
|
||||
| you can define the default placeholder view for all components below.
|
||||
|
|
||||
*/
|
||||
|
||||
'component_placeholder' => null, // Example: 'placeholders::skeleton'
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Make Command
|
||||
|---------------------------------------------------------------------------
|
||||
| This value determines the default configuration for the artisan make command
|
||||
| You can configure the component type (sfc, mfc, class) and whether to use
|
||||
| the high-voltage (⚡) emoji as a prefix in the sfc|mfc component names.
|
||||
|
|
||||
*/
|
||||
|
||||
'make_command' => [
|
||||
'type' => 'sfc', // Options: 'sfc', 'mfc', 'class'
|
||||
'emoji' => true, // Options: true, false
|
||||
'with' => [
|
||||
'js' => false,
|
||||
'css' => false,
|
||||
'test' => false,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Class Namespace
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value sets the root class namespace for Livewire component classes in
|
||||
| your application. This value will change where component auto-discovery
|
||||
| finds components. It's also referenced by the file creation commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'class_namespace' => 'App\\Livewire',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Class Path
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value is used to specify the path where Livewire component class files
|
||||
| are created when running creation commands like `artisan make:livewire`.
|
||||
| This path is customizable to match your projects directory structure.
|
||||
|
|
||||
*/
|
||||
|
||||
'class_path' => app_path('Livewire'),
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| View Path
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value is used to specify where Livewire component Blade templates are
|
||||
| stored when running file creation commands like `artisan make:livewire`.
|
||||
| It is also used if you choose to omit a component's render() method.
|
||||
|
|
||||
*/
|
||||
|
||||
'view_path' => resource_path('views/livewire'),
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Temporary File Uploads
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire handles file uploads by storing uploads in a temporary directory
|
||||
| before the file is stored permanently. All file uploads are directed to
|
||||
| a global endpoint for temporary storage. You may configure this below:
|
||||
|
|
||||
*/
|
||||
|
||||
'temporary_file_upload' => [
|
||||
'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default'
|
||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||
],
|
||||
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Render On Redirect
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines if Livewire will run a component's `render()` method
|
||||
| after a redirect has been triggered using something like `redirect(...)`
|
||||
| Setting this to true will render the view once more before redirecting
|
||||
|
|
||||
*/
|
||||
|
||||
'render_on_redirect' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Eloquent Model Binding
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Previous versions of Livewire supported binding directly to eloquent model
|
||||
| properties using wire:model by default. However, this behavior has been
|
||||
| deemed too "magical" and has therefore been put under a feature flag.
|
||||
|
|
||||
*/
|
||||
|
||||
'legacy_model_binding' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Auto-inject Frontend Assets
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| By default, Livewire automatically injects its JavaScript and CSS into the
|
||||
| <head> and <body> of pages containing Livewire components. By disabling
|
||||
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
||||
|
|
||||
*/
|
||||
|
||||
'inject_assets' => true,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Navigate (SPA mode)
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| By adding `wire:navigate` to links in your Livewire application, Livewire
|
||||
| will prevent the default link handling and instead request those pages
|
||||
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
||||
|
|
||||
*/
|
||||
|
||||
'navigate' => [
|
||||
'show_progress_bar' => true,
|
||||
'progress_bar_color' => '#2299dd',
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| HTML Morph Markers
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
|
||||
| after each update. To make this process more reliable, Livewire injects
|
||||
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
||||
|
|
||||
*/
|
||||
|
||||
'inject_morph_markers' => true,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Smart Wire Keys
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire uses loops and keys used within loops to generate smart keys that
|
||||
| are applied to nested components that don't have them. This makes using
|
||||
| nested components more reliable by ensuring that they all have keys.
|
||||
|
|
||||
*/
|
||||
|
||||
'smart_wire_keys' => true,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Pagination Theme
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| When enabling Livewire's pagination feature by using the `WithPagination`
|
||||
| trait, Livewire will use Tailwind templates to render pagination views
|
||||
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
||||
|
|
||||
*/
|
||||
|
||||
'pagination_theme' => 'tailwind',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Release Token
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This token is stored client-side and sent along with each request to check
|
||||
| a users session to see if a new release has invalidated it. If there is
|
||||
| a mismatch it will throw an error and prompt for a browser refresh.
|
||||
|
|
||||
*/
|
||||
|
||||
'release_token' => 'a',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| CSP Safe
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This config is used to determine if Livewire will use the CSP-safe version
|
||||
| of Alpine in its bundle. This is useful for applications that are using
|
||||
| strict Content Security Policy (CSP) to protect against XSS attacks.
|
||||
|
|
||||
*/
|
||||
|
||||
'csp_safe' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Payload Guards
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| These settings protect against malicious or oversized payloads that could
|
||||
| cause denial of service. The default values should feel reasonable for
|
||||
| most web applications. Each can be set to null to disable the limit.
|
||||
|
|
||||
*/
|
||||
|
||||
'payload' => [
|
||||
'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes
|
||||
'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths
|
||||
'max_calls' => 50, // Maximum method calls per request
|
||||
'max_components' => 20, // Maximum components per batch request
|
||||
],
|
||||
];
|
||||
14
opencode.json
Normal file
14
opencode.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"laravel-boost": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"vendor/bin/sail",
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
197
resources/views/layouts/app.blade.php
Normal file
197
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{!! seo($seo ?? null) !!}
|
||||
|
||||
<title>{{ $title ?? 'Page Title' }}</title>
|
||||
@livewireStyles
|
||||
@wireUiScripts
|
||||
@stack('scripts')
|
||||
@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>
|
||||
@include('components.layouts.partials.styles')
|
||||
</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', inboxSidebarOpen: false }"
|
||||
x-init="$watch('sidebarExpanded', value => localStorage.setItem('sidebar-expanded', value))"
|
||||
>
|
||||
<x-dialog />
|
||||
<x-notifications />
|
||||
<script>
|
||||
if (localStorage.getItem('sidebar-expanded') == 'true') {
|
||||
document.querySelector('body').classList.add('sidebar-expanded');
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('sidebar-expanded');
|
||||
}
|
||||
</script>
|
||||
<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">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16 lg:border-b border-gray-200 dark:border-gray-700/60">
|
||||
|
||||
<!-- Header: Left side -->
|
||||
<div class="flex">
|
||||
<!-- Hamburger button -->
|
||||
<button
|
||||
class="text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 lg:hidden"
|
||||
@click.stop="sidebarOpen = !sidebarOpen"
|
||||
aria-controls="sidebar"
|
||||
:aria-expanded="sidebarOpen"
|
||||
>
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<svg class="w-6 h-6 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="5" width="16" height="2"/>
|
||||
<rect x="4" y="11" width="16" height="2"/>
|
||||
<rect x="4" y="17" width="16" height="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Header: Right side -->
|
||||
<div class="flex items-center space-x-3">
|
||||
|
||||
{{--@include('components.layouts.partials.search-button')--}}
|
||||
|
||||
{{--@include('components.layouts.partials.notification-buttons')--}}
|
||||
|
||||
|
||||
@if(\App\Support\NostrAuth::check())
|
||||
<form method="post" action="{{ route('logout') }}"
|
||||
@submit="$dispatch('nostrLoggedOut')">
|
||||
@csrf
|
||||
<x-button secondary label="Logout" type="submit"/>
|
||||
</form>
|
||||
@else
|
||||
<x-button wire:key="loginBtn" label="Mit Nostr verbinden" @click="openNostrLogin"
|
||||
x-show="!$store.nostr.user"/>
|
||||
@endif
|
||||
|
||||
<!-- Info button -->
|
||||
<div class="relative inline-flex" x-data="{ open: false }">
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center hover:bg-[#1B1B1B] lg:hover:bg-[#1B1B1B] dark:hover:bg-[#1B1B1B]/50 dark:lg:hover:bg-[#1B1B1B] rounded-full"
|
||||
:class="{ 'bg-gray-200 dark:bg-[#1B1B1B]': open }"
|
||||
aria-haspopup="true"
|
||||
@click.prevent="open = !open"
|
||||
:aria-expanded="open"
|
||||
>
|
||||
<span class="sr-only">Info</span>
|
||||
<svg class="fill-current text-gray-500/80 dark:text-gray-400/80" width="16" height="16"
|
||||
viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9 7.5a1 1 0 1 0-2 0v4a1 1 0 1 0 2 0v-4ZM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
|
||||
<path fill-rule="evenodd"
|
||||
d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16Zm6-8A6 6 0 1 1 2 8a6 6 0 0 1 12 0Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="origin-top-right z-10 absolute top-full right-0 min-w-44 bg-white dark:bg-[#1B1B1B] border border-gray-200 dark:border-[#1B1B1B]/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
|
||||
>
|
||||
<div
|
||||
class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase pt-1.5 pb-2 px-3">
|
||||
Information
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
|
||||
target="_blank"
|
||||
href="https://gitworkshop.dev/r/naddr1qvzqqqrhnypzqzklvar4enzu53t06vpzu3h465nwkzhk9p9ls4y5crwhs3lnu5pnqy88wumn8ghj7mn0wvhxcmmv9uqpxetfde6kuer6wasku7nfvukkummnw3eqdgsn8w/issues"
|
||||
@click="open = false" @focus="open = true"
|
||||
@focusout="open = false">
|
||||
<i class="fa-sharp-duotone fa-solid fa-code w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
|
||||
<span>Issues/Feedback</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
|
||||
href="{{ route('changelog') }}" @click="open = false" @focus="open = true"
|
||||
@focusout="open = false">
|
||||
<i class="fa-sharp-duotone fa-solid fa-code w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
|
||||
<span>Changelog</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
|
||||
href="https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr"
|
||||
target="_blank" @click="open = false" @focus="open = true"
|
||||
@focusout="open = false">
|
||||
<i class="fa-brands fa-github w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
|
||||
<span>Github</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
|
||||
href="https://einundzwanzig.space/kontakt/" target="_blank"
|
||||
@click="open = false" @focus="open = true"
|
||||
@focusout="open = false">
|
||||
<i class="fa-sharp-duotone fa-solid fa-info w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
|
||||
<span>Impressum</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{--@include('components.layouts.partials.dark-mode-toggle')--}}
|
||||
|
||||
<!-- Divider -->
|
||||
{{--<hr class="w-px h-6 bg-gray-200 dark:bg-gray-700/60 border-none"/>--}}
|
||||
|
||||
{{--@include('components.layouts.partials.user-button')--}}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="grow">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScriptConfig
|
||||
<script>
|
||||
window.wnjParams = {
|
||||
position: 'bottom',
|
||||
// The only accepted value is 'bottom', default is top
|
||||
accent: 'orange',
|
||||
// Supported values: cyan (default), green, purple, red, orange, neutral, stone
|
||||
startHidden: false,
|
||||
// If the host page has a button that call `getPublicKey` to start a
|
||||
// login procedure, the minimized widget can be hidden until connected
|
||||
compactMode: false,
|
||||
// Show the minimized widget in a compact form
|
||||
disableOverflowFix: false,
|
||||
// If the host page on mobile has an horizontal scrolling, the floating
|
||||
// element/modal are pushed to the extreme right/bottom and exit the
|
||||
// viewport. A style is injected in the html/body elements fix this.
|
||||
// This option permit to disable this default behavior
|
||||
}
|
||||
</script>
|
||||
<script src="{{ asset('dist/window.nostr.min.js.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
<div>
|
||||
<?php
|
||||
$positions = [
|
||||
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
||||
'board' => ['icon' => 'fa-users', 'title' => 'Vorstandsmitglieder'],
|
||||
];
|
||||
?>
|
||||
|
||||
<?php if ($isAllowed): ?>
|
||||
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="electionAdminCharts()">
|
||||
|
||||
<!-- Dashboard actions -->
|
||||
<div class="sm:flex sm:justify-between sm:items-center mb-8">
|
||||
|
||||
<!-- 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">
|
||||
Wahl des Vorstands <?php echo e($election->year); ?>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$president = $positions['presidency'];
|
||||
$board = $positions['board'];
|
||||
?>
|
||||
|
||||
<!-- Cards -->
|
||||
<div class="grid gap-y-4">
|
||||
<div wire:key="presidency" wire:ignore
|
||||
class="flex flex-col bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
||||
class="fa-sharp-duotone fa-solid <?php echo e($president['icon']); ?> w-5 h-5 fill-current text-white mr-4"></i><?php echo e($president['title']); ?>
|
||||
</h2>
|
||||
</header>
|
||||
<div class="grow">
|
||||
<!-- Change the height attribute to adjust the chart height -->
|
||||
<canvas x-ref="chart_presidency" width="724" height="288"
|
||||
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div wire:key="board" wire:ignore
|
||||
class="flex flex-col bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
||||
class="fa-sharp-duotone fa-solid <?php echo e($board['icon']); ?> w-5 h-5 fill-current text-white mr-4"></i><?php echo e($board['title']); ?>
|
||||
</h2>
|
||||
</header>
|
||||
<div class="grow">
|
||||
<!-- Change the height attribute to adjust the chart height -->
|
||||
<canvas x-ref="chart_board" width="724" height="288"
|
||||
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">Mitglieder</h3>
|
||||
<p class="mt-1 max-w">
|
||||
Du bist nicht berechtigt, Mitglieder zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<x-layouts.app title="{{ __('Wahlen') }}">
|
||||
<div>
|
||||
<?php if($isAllowed): ?>
|
||||
<div class="relative flex h-full">
|
||||
<?php foreach($elections as $election): ?>
|
||||
<div class="w-full sm:w-1/3 p-4">
|
||||
<div class="shadow-lg rounded-lg overflow-hidden">
|
||||
<?php echo e($election['year']); ?>
|
||||
</div>
|
||||
<div class="shadow-lg rounded-lg overflow-hidden">
|
||||
<x-textarea wire:model="elections.<?php echo e($loop->index); ?>.candidates" rows="25"
|
||||
label="candidates" placeholder=""/>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<x-button label="Speichern" wire:click="saveElection(<?php echo e($loop->index); ?>)"/>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">Einstellungen</h3>
|
||||
<p class="mt-1 max-w">
|
||||
Du bist nicht berechtigt, die Einstellungen zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -1,177 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
use swentel\nostr\{
|
||||
Filter\Filter,
|
||||
Key\Key,
|
||||
Message\EventMessage,
|
||||
Message\RequestMessage,
|
||||
Relay\Relay,
|
||||
Relay\RelaySet,
|
||||
Request\Request,
|
||||
Subscription\Subscription,
|
||||
Event\Event as NostrEvent,
|
||||
Sign\Sign
|
||||
};
|
||||
|
||||
use function Livewire\Volt\{computed, mount, state, with, updated, on};
|
||||
use function Laravel\Folio\{middleware, name};
|
||||
|
||||
name('association.election');
|
||||
|
||||
state([
|
||||
'isAllowed' => false,
|
||||
'showLog' => false,
|
||||
'currentPubkey' => null,
|
||||
'currentPleb' => null,
|
||||
'events' => [],
|
||||
'boardEvents' => [],
|
||||
'election' => fn() => $election,
|
||||
'plebs' => [],
|
||||
'search' => '',
|
||||
'signThisEvent' => '',
|
||||
'isNotClosed' => true,
|
||||
]);
|
||||
|
||||
mount(function () {
|
||||
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
||||
->with(['profile'])
|
||||
->whereIn('association_status', [3, 4])
|
||||
->orderBy('association_status', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
$this->loadEvents();
|
||||
$this->loadBoardEvents();
|
||||
if ($this->election->end_time?->isPast() || !config('services.voting')) {
|
||||
$this->isNotClosed = false;
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$logPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||
$this->showLog = true;
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
},
|
||||
'echo:votes,.newVote' => function () {
|
||||
$this->loadEvents();
|
||||
$this->loadBoardEvents();
|
||||
},
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
updated([
|
||||
'search' => function ($value) {
|
||||
$this->plebs = \App\Models\EinundzwanzigPleb::query()
|
||||
->with(['profile'])
|
||||
->whereIn('association_status', [3, 4])
|
||||
->where(fn($query)
|
||||
=> $query
|
||||
->where('pubkey', 'like', "%$value%")
|
||||
->orWhereHas('profile', fn($query) => $query->where('name', 'ilike', "%$value%")))
|
||||
->orderBy('association_status', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
},
|
||||
]);
|
||||
|
||||
$loadEvents = function () {
|
||||
$this->events = $this->loadNostrEvents([32122]);
|
||||
};
|
||||
|
||||
$loadBoardEvents = function () {
|
||||
$this->boardEvents = $this->loadNostrEvents([2121]);
|
||||
};
|
||||
|
||||
$loadNostrEvents = function ($kinds) {
|
||||
$subscription = new Subscription();
|
||||
$subscriptionId = $subscription->setId();
|
||||
$filter = new Filter();
|
||||
$filter->setKinds($kinds);
|
||||
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
||||
$relaySet = new RelaySet();
|
||||
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
||||
$request = new Request($relaySet, $requestMessage);
|
||||
$response = $request->send();
|
||||
return collect($response[config('services.relay')])
|
||||
->map(function($event) {
|
||||
if(!isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$vote = function ($pubkey, $type, $board = false) {
|
||||
if ($this->election->end_time?->isPast()) {
|
||||
$this->isNotClosed = false;
|
||||
return;
|
||||
}
|
||||
$note = new NostrEvent();
|
||||
$note->setKind($board ? 2121 : 32122);
|
||||
if (!$board) {
|
||||
$dTag = sprintf('%s,%s,%s', $this->currentPleb->pubkey, date('Y'), $type);
|
||||
$note->setTags([['d', $dTag]]);
|
||||
}
|
||||
$note->setContent("$pubkey,$type");
|
||||
$this->signThisEvent = $note->toJson();
|
||||
};
|
||||
|
||||
$checkElection = function () {
|
||||
if ($this->election->end_time?->isPast()) {
|
||||
$this->isNotClosed = false;
|
||||
}
|
||||
};
|
||||
|
||||
$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);
|
||||
$relay = new Relay(config('services.relay'));
|
||||
$relay->setMessage($eventMessage);
|
||||
$relay->send();
|
||||
Broadcast::on('votes')->as('newVote')->sendNow();
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app
|
||||
:seo="new \RalphJSmit\Laravel\SEO\Support\SEOData(title: 'Wahlen ' . $election->year, description: 'Wahlen des Vereins im Jahr ' . $election->year)"
|
||||
>
|
||||
@volt
|
||||
<div>
|
||||
@if($isAllowed)
|
||||
<?php if($isAllowed): ?>
|
||||
<div x-cloak class="relative flex h-full" x-data="nostrApp(@this)"
|
||||
wire:poll.600000ms="checkElection">
|
||||
|
||||
@php
|
||||
<?php
|
||||
$positions = [
|
||||
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
||||
'board' => ['icon' => 'fa-users', 'title' => 'Vizepräsidium'],
|
||||
@@ -227,7 +62,7 @@ $signEvent = function ($event) {
|
||||
})
|
||||
->sortByDesc('created_at')
|
||||
->values();
|
||||
@endphp
|
||||
?>
|
||||
|
||||
<!-- Inbox sidebar -->
|
||||
<div id="inbox-sidebar"
|
||||
@@ -319,11 +154,11 @@ $signEvent = function ($event) {
|
||||
Plebs
|
||||
</div>
|
||||
<ul class="mb-6">
|
||||
@foreach($plebs as $pleb)
|
||||
<?php 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/test' }}"
|
||||
src="<?php echo e($pleb['profile']['picture'] ?? 'https://robohash.org/test'); ?>"
|
||||
onerror="this.onerror=null; this.src='https://robohash.org/test';"
|
||||
width="32"
|
||||
height="32"
|
||||
@@ -332,34 +167,34 @@ $signEvent = function ($event) {
|
||||
<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>
|
||||
class="text-sm font-semibold text-gray-800 dark:text-gray-100 truncate"><?php echo e($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() }}"/>
|
||||
color="<?php echo e(\App\Enums\AssociationStatus::from($pleb['association_status'])->color()); ?>"
|
||||
label="<?php echo e(\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
|
||||
<?php 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
|
||||
wire:key="p_<?php echo e($name); ?>">
|
||||
<?php if($votedResult): ?>
|
||||
<i class="fa-sharp-duotone fa-solid <?php echo e($p['icon']); ?> w-4 h-4 fill-current text-green-500"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -369,9 +204,9 @@ $signEvent = function ($event) {
|
||||
</div>
|
||||
|
||||
<!-- Inbox body -->
|
||||
@if($currentPubkey)
|
||||
<?php if($currentPubkey): ?>
|
||||
|
||||
@php
|
||||
<?php
|
||||
$electionConfig = collect(json_decode($election->candidates, true, 512, JSON_THROW_ON_ERROR))
|
||||
->map(function ($c) use ($loadedEvents, $currentPubkey) {
|
||||
$candidates = \App\Models\Profile::query()
|
||||
@@ -425,7 +260,7 @@ $signEvent = function ($event) {
|
||||
'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'">
|
||||
@@ -437,12 +272,12 @@ $signEvent = function ($event) {
|
||||
<div
|
||||
class="flex flex-col space-y-2 sm:space-y-0 sm:flex-row justify-between items-center w-full">
|
||||
<div>
|
||||
@if($isNotClosed)
|
||||
<?php if($isNotClosed): ?>
|
||||
<x-badge success
|
||||
label="Die Wahl ist geöffnet bis zum {{ $election->end_time?->timezone('Europe/Berlin')->format('d.m.Y H:i') }}"/>
|
||||
@else
|
||||
label="Die Wahl ist geöffnet bis zum <?php echo e($election->end_time?->timezone('Europe/Berlin')->format('d.m.Y H:i')); ?>"/>
|
||||
<?php else: ?>
|
||||
<x-badge negative label="Die Wahl ist geschlossen"/>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<x-button secondary
|
||||
@@ -476,52 +311,52 @@ $signEvent = function ($event) {
|
||||
<h1 class="text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1 sm:mb-0 ml-2">
|
||||
Wahl des Präsidiums
|
||||
</h1>
|
||||
@php
|
||||
<?php
|
||||
$president = $positions['presidency'];
|
||||
$board = $positions['board'];
|
||||
@endphp
|
||||
?>
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
<div
|
||||
class="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 {{ $president['icon'] }} w-9 h-9 fill-current text-white"></i>
|
||||
<i class="fa-sharp-duotone fa-solid <?php echo e($president['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">{{ $president['title'] }}</h2>
|
||||
<h2 class="text-xl leading-snug font-semibold"><?php echo e($president['title']); ?></h2>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
@php
|
||||
<?php
|
||||
$votedResult = $loadedEvents->filter(fn ($event) => $event['pubkey'] === $currentPubkey)->firstWhere('type', 'presidency');
|
||||
@endphp
|
||||
@if($votedResult)
|
||||
<span>Du hast "{{ $votedResult['votedFor']['name'] ?? 'error' }}" gewählt</span>
|
||||
@else
|
||||
?>
|
||||
<?php if($votedResult): ?>
|
||||
<span>Du hast "<?php echo e($votedResult['votedFor']['name'] ?? 'error'); ?>" gewählt</span>
|
||||
<?php else: ?>
|
||||
<span>Wähle deinen Kandidaten für das Präsidium.</span>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="mt-5">
|
||||
<div class="grid sm:grid-cols-2 gap-2">
|
||||
@foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] ?? [] as $c)
|
||||
<?php foreach($electionConfig->firstWhere('type', 'presidency')['candidates'] ?? [] as $c): ?>
|
||||
<div
|
||||
@if($isNotClosed)wire:click="vote('{{ $c['pubkey'] }}', 'presidency')"
|
||||
@endif
|
||||
class="{{ $c['votedClass'] }} cursor-pointer text-xs inline-flex font-medium rounded-full text-center px-2.5 py-1">
|
||||
<?php if($isNotClosed): ?>wire:click="vote('<?php echo e($c['pubkey']); ?>', 'presidency')"
|
||||
<?php endif; ?>
|
||||
class="<?php echo e($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'] }}';"
|
||||
src="<?php echo e($c['picture'] ?? 'https://robohash.org/' . $c['pubkey']); ?>"
|
||||
onerror="this.onerror=null; this.src='https://robohash.org/<?php echo e($c['pubkey']); ?>';"
|
||||
width="24" height="24"
|
||||
alt="{{ $c['name'] }}"/>
|
||||
{{ $c['name'] }}
|
||||
alt="<?php echo e($c['name']); ?>"/>
|
||||
<?php echo e($c['name']); ?>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -543,21 +378,21 @@ $signEvent = function ($event) {
|
||||
</div>
|
||||
<footer class="mt-5">
|
||||
<div class="grid sm:grid-cols-4 gap-2">
|
||||
@foreach($electionConfigBoard->firstWhere('type', 'board')['candidates'] ?? [] as $c)
|
||||
<?php foreach($electionConfigBoard->firstWhere('type', 'board')['candidates'] ?? [] as $c): ?>
|
||||
<div
|
||||
@if($isNotClosed && !$c['hasVoted'])wire:click="vote('{{ $c['pubkey'] }}', 'board', true)"
|
||||
@endif
|
||||
class="{{ $c['votedClass'] }} cursor-pointer text-xs inline-flex font-medium rounded-full text-center px-2.5 py-1">
|
||||
<?php if($isNotClosed && !$c['hasVoted']): ?>wire:click="vote('<?php echo e($c['pubkey']); ?>', 'board', true)"
|
||||
<?php endif; ?>
|
||||
class="<?php echo e($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'] }}';"
|
||||
src="<?php echo e($c['picture'] ?? 'https://robohash.org/' . $c['pubkey']); ?>"
|
||||
onerror="this.onerror=null; this.src='https://robohash.org/<?php echo e($c['pubkey']); ?>';"
|
||||
width="24" height="24"
|
||||
alt="{{ $c['name'] }}"/>
|
||||
{{ $c['name'] }}
|
||||
alt="<?php echo e($c['name']); ?>"/>
|
||||
<?php echo e($c['name']); ?>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -573,7 +408,7 @@ $signEvent = function ($event) {
|
||||
<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">Präsidium Log <span
|
||||
class="text-gray-400 dark:text-gray-500 font-medium">{{ $loadedEvents->count() }}</span>
|
||||
class="text-gray-400 dark:text-gray-500 font-medium"><?php echo e($loadedEvents->count()); ?></span>
|
||||
</h2>
|
||||
</header>
|
||||
<div>
|
||||
@@ -607,29 +442,29 @@ $signEvent = function ($event) {
|
||||
</thead>
|
||||
<!-- Table body -->
|
||||
<tbody class="text-sm">
|
||||
@foreach($loadedEvents as $event)
|
||||
<?php 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>
|
||||
class="font-medium"><?php echo e(\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>
|
||||
<div><?php echo e($event['kind']); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['profile']['name'] ?? '' }}</div>
|
||||
<div><?php echo e($event['profile']['name'] ?? ''); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['created_at'] }}</div>
|
||||
<div><?php echo e($event['created_at']); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['votedFor']['name'] ?? '' }}</div>
|
||||
<div><?php echo e($event['votedFor']['name'] ?? ''); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['type'] }}</div>
|
||||
<div><?php echo e($event['type']); ?></div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -640,7 +475,7 @@ $signEvent = function ($event) {
|
||||
<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">Board Log <span
|
||||
class="text-gray-400 dark:text-gray-500 font-medium">{{ $loadedBoardEvents->count() }}</span>
|
||||
class="text-gray-400 dark:text-gray-500 font-medium"><?php echo e($loadedBoardEvents->count()); ?></span>
|
||||
</h2>
|
||||
</header>
|
||||
<div>
|
||||
@@ -674,29 +509,29 @@ $signEvent = function ($event) {
|
||||
</thead>
|
||||
<!-- Table body -->
|
||||
<tbody class="text-sm">
|
||||
@foreach($loadedBoardEvents as $event)
|
||||
<?php foreach($loadedBoardEvents 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>
|
||||
class="font-medium"><?php echo e(\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>
|
||||
<div><?php echo e($event['kind']); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['profile']['name'] ?? '' }}</div>
|
||||
<div><?php echo e($event['profile']['name'] ?? ''); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['created_at'] }}</div>
|
||||
<div><?php echo e($event['created_at']); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['votedFor']['name'] ?? '' }}</div>
|
||||
<div><?php echo e($event['votedFor']['name'] ?? ''); ?></div>
|
||||
</td>
|
||||
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
|
||||
<div>{{ $event['type'] }}</div>
|
||||
<div><?php echo e($event['type']); ?></div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -707,10 +542,10 @@ $signEvent = function ($event) {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
@@ -721,8 +556,7 @@ $signEvent = function ($event) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
20
resources/views/livewire/association/members/admin.blade.php
Normal file
20
resources/views/livewire/association/members/admin.blade.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<x-layouts.app title="{{ __('Mitglieder') }}">
|
||||
<div>
|
||||
<?php if($isAllowed): ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<livewire:einundzwanzig-pleb-table/>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">Mitglieder</h3>
|
||||
<p class="mt-1 max-w">
|
||||
Du bist nicht berechtigt, Mitglieder zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -1,122 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\NotificationForm;
|
||||
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 WireUi\Actions\Notification as WireNotification;
|
||||
|
||||
use function Laravel\Folio\{middleware, name};
|
||||
use function Livewire\Volt\{state, mount, on, computed, form, usesFileUploads};
|
||||
|
||||
name('news');
|
||||
|
||||
form(NotificationForm::class);
|
||||
|
||||
usesFileUploads();
|
||||
|
||||
state([
|
||||
'file',
|
||||
'news' => fn()
|
||||
=> \App\Models\Notification::query()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get(),
|
||||
'isAllowed' => false,
|
||||
'canEdit' => false,
|
||||
'currentPubkey' => null,
|
||||
'currentPleb' => null,
|
||||
]);
|
||||
|
||||
mount(function () {
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
if (in_array($this->currentPleb->npub, config('einundzwanzig.config.current_board'), true)) {
|
||||
$this->canEdit = true;
|
||||
}
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
if (in_array($this->currentPleb->npub, config('einundzwanzig.config.current_board'), true)) {
|
||||
$this->canEdit = true;
|
||||
}
|
||||
$this->isAllowed = true;
|
||||
},
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
$save = function () {
|
||||
$this->form->validate();
|
||||
|
||||
$this->validate([
|
||||
'file' => 'required|file|mimes:pdf|max:1024',
|
||||
]);
|
||||
|
||||
$notification = \App\Models\Notification::query()
|
||||
->orderBy('created_at', 'desc')
|
||||
->create([
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
'category' => $this->form->category,
|
||||
'name' => $this->form->name,
|
||||
'description' => $this->form->description,
|
||||
]);
|
||||
|
||||
$notification
|
||||
->addMedia($this->file->getRealPath())
|
||||
->usingName($this->file->getClientOriginalName())
|
||||
->toMediaCollection('pdf');
|
||||
|
||||
$this->form->reset();
|
||||
$this->file = null;
|
||||
|
||||
$this->news = \App\Models\Notification::query()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
};
|
||||
|
||||
$delete = function ($id) {
|
||||
$notification = new WireNotification($this);
|
||||
$notification->confirm([
|
||||
'title' => 'Post löschen',
|
||||
'message' => 'Bist du sicher, dass du diesen Post löschen möchtest?',
|
||||
'accept' => [
|
||||
'label' => 'Ja, löschen',
|
||||
'method' => 'deleteNow',
|
||||
'params' => $id,
|
||||
],
|
||||
]);
|
||||
};
|
||||
|
||||
$deleteNow = function ($id) {
|
||||
$notification = \App\Models\Notification::query()->find($id);
|
||||
$notification->delete();
|
||||
$this->news = \App\Models\Notification::query()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app
|
||||
:seo="new \RalphJSmit\Laravel\SEO\Support\SEOData(title: 'News', description: 'Die News des Vereins.')"
|
||||
>
|
||||
@volt
|
||||
<div>
|
||||
@if($isAllowed)
|
||||
<?php if($isAllowed): ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 md:py-0 w-full max-w-9xl mx-auto">
|
||||
|
||||
<div class="xl:flex">
|
||||
@@ -151,17 +37,17 @@ $deleteNow = function ($id) {
|
||||
Menu
|
||||
</div>
|
||||
<ul class="flex flex-nowrap md:block mr-3 md:mr-0">
|
||||
@foreach(\App\Enums\NewsCategory::selectOptions() as $category)
|
||||
<?php foreach(\App\Enums\NewsCategory::selectOptions() as $category): ?>
|
||||
<li class="mr-0.5 md:mr-0 md:mb-0.5"
|
||||
wire:key="category_{{ $category['value'] }}">
|
||||
wire:key="category_<?php echo e($category['value']); ?>">
|
||||
<div
|
||||
class="flex items-center px-2.5 py-2 rounded-lg whitespace-nowrap bg-white dark:bg-gray-800">
|
||||
<i class="fa-sharp-duotone fa-solid fa-{{ $category['icon'] }} shrink-0 fill-current text-amber-500 mr-2"></i>
|
||||
<i class="fa-sharp-duotone fa-solid fa-<?php echo e($category['icon']); ?> shrink-0 fill-current text-amber-500 mr-2"></i>
|
||||
<span
|
||||
class="text-sm font-medium text-amber-500">{{ $category['label'] }}</span>
|
||||
class="text-sm font-medium text-amber-500"><?php echo e($category['label']); ?></span>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,25 +60,27 @@ $deleteNow = function ($id) {
|
||||
<div class="md:py-8">
|
||||
|
||||
<div class="space-y-2">
|
||||
@forelse($news as $post)
|
||||
<article wire:key="post_{{ $post->id }}"
|
||||
<?php $__empty_1 = true; ?>
|
||||
<?php foreach($news as $post): ?>
|
||||
<?php $__empty_1 = false; ?>
|
||||
<article wire:key="post_<?php echo e($post->id); ?>"
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-xl p-5">
|
||||
<div class="flex flex-start space-x-4">
|
||||
<!-- Avatar -->
|
||||
<div class="shrink-0 mt-1.5">
|
||||
<img class="w-8 h-8 rounded-full"
|
||||
src="{{ $post->einundzwanzigPleb->profile?->picture ?? asset('einundzwanzig-alpha.jpg') }}"
|
||||
src="<?php echo e($post->einundzwanzigPleb->profile?->picture ?? asset('einundzwanzig-alpha.jpg')); ?>"
|
||||
width="32" height="32"
|
||||
alt="{{ $post->einundzwanzigPleb->profile?->name }}">
|
||||
alt="<?php echo e($post->einundzwanzigPleb->profile?->name); ?>">
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="grow">
|
||||
<!-- Title -->
|
||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100 mb-2">
|
||||
{{ $post->name }}
|
||||
<?php echo e($post->name); ?>
|
||||
</h2>
|
||||
<p class="mb-6">
|
||||
{{ $post->description }}
|
||||
<?php echo e($post->description); ?>
|
||||
</p>
|
||||
<!-- Footer -->
|
||||
<footer class="flex flex-wrap text-sm">
|
||||
@@ -207,14 +95,14 @@ $deleteNow = function ($id) {
|
||||
<path
|
||||
d="M15.686 5.708 10.291.313c-.4-.4-.999-.4-1.399 0s-.4 1 0 1.399l.6.6-6.794 3.696-1-1C1.299 4.61.7 4.61.3 5.009c-.4.4-.4 1 0 1.4l1.498 1.498 2.398 2.398L.6 14.001 2 15.4l3.696-3.697L9.692 15.7c.5.5 1.199.2 1.398 0 .4-.4.4-1 0-1.4l-.999-.998 3.697-6.695.6.6c.599.6 1.199.2 1.398 0 .3-.4.3-1.1-.1-1.499Zm-7.193 6.095L4.196 7.507l6.695-3.697 1.298 1.299-3.696 6.694Z"></path>
|
||||
</svg>
|
||||
{{ $post->einundzwanzigPleb->profile->name }}
|
||||
<?php echo e($post->einundzwanzigPleb->profile->name); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center after:block after:content-['·'] last:after:content-[''] after:text-sm after:text-gray-400 dark:after:text-gray-600 after:px-2">
|
||||
<span
|
||||
class="text-gray-500">{{ $post->created_at->format('d.m.Y') }}</span>
|
||||
class="text-gray-500"><?php echo e($post->created_at->format('d.m.Y')); ?></span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -226,20 +114,21 @@ $deleteNow = function ($id) {
|
||||
:href="url()->temporarySignedRoute('dl', now()->addMinutes(30), ['media' => $post->getFirstMedia('pdf')])"
|
||||
label="Öffnen"
|
||||
primary icon="cloud-arrow-down"/>
|
||||
@if($canEdit)
|
||||
<?php if($canEdit): ?>
|
||||
<x-button
|
||||
xs
|
||||
wire:click="delete({{ $post->id }})"
|
||||
wire:click="delete(<?php echo e($post->id); ?>)"
|
||||
label="Löschen"
|
||||
negative icon="trash"/>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<?php endforeach; ?>
|
||||
<?php if($__empty_1): ?>
|
||||
<article class="bg-white dark:bg-gray-800 shadow-sm rounded-xl p-5">
|
||||
<p>Keine News vorhanden.</p>
|
||||
</article>
|
||||
@endforelse
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -256,7 +145,7 @@ $deleteNow = function ($id) {
|
||||
<!-- Blocks -->
|
||||
<div class="space-y-4">
|
||||
|
||||
@if($canEdit)
|
||||
<?php if($canEdit): ?>
|
||||
<div class="bg-white dark:bg-gray-800 p-4 rounded-xl">
|
||||
<div
|
||||
class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase mb-4">
|
||||
@@ -265,8 +154,16 @@ $deleteNow = function ($id) {
|
||||
<div class="mt-4 flex flex-col space-y-2">
|
||||
<div>
|
||||
<input class="text-gray-200" type="file" wire:model="file">
|
||||
@error('file') <span
|
||||
class="text-red-500">{{ $message }}</span> @enderror
|
||||
<?php $__errorArgs = ['file'];
|
||||
$__bag = $errors->getBag($__errorProps ?? 'default');
|
||||
if ($__bag->has($__errorArgs)) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs); ?>
|
||||
<span class="text-red-500"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<div>
|
||||
<x-native-select
|
||||
@@ -292,7 +189,7 @@ $deleteNow = function ($id) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,7 +199,7 @@ $deleteNow = function ($id) {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
@@ -315,7 +212,6 @@ $deleteNow = function ($id) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,287 +1,6 @@
|
||||
<?php
|
||||
|
||||
use swentel\nostr\Event\Event as NostrEvent;
|
||||
use swentel\nostr\Filter\Filter;
|
||||
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\Sign\Sign;
|
||||
use swentel\nostr\Subscription\Subscription;
|
||||
use WireUi\Actions\Notification;
|
||||
|
||||
use function Laravel\Folio\{name};
|
||||
use function Livewire\Volt\{form, mount, on, state, updated};
|
||||
|
||||
name('association.profile');
|
||||
|
||||
state([
|
||||
'no' => false,
|
||||
'showEmail' => true,
|
||||
'fax' => '',
|
||||
'email' => '',
|
||||
'yearsPaid' => [],
|
||||
'events' => [],
|
||||
'payments' => [],
|
||||
'amountToPay' => config('app.env') === 'production' ? 21000 : 1,
|
||||
'currentYearIsPaid' => false,
|
||||
'currentPubkey' => null,
|
||||
'currentPleb' => null,
|
||||
]);
|
||||
|
||||
form(\App\Livewire\Forms\ApplicationForm::class);
|
||||
|
||||
mount(function () {
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->with([
|
||||
'paymentEvents' => fn($query)
|
||||
=> $query->where('year', date('Y')),
|
||||
])
|
||||
->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->email = $this->currentPleb->email;
|
||||
$this->no = $this->currentPleb->no_email;
|
||||
$this->showEmail = !$this->no;
|
||||
if ($this->currentPleb->association_status === \App\Enums\AssociationStatus::ACTIVE) {
|
||||
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
|
||||
}
|
||||
if ($this->currentPleb->paymentEvents->count() < 1) {
|
||||
$this->createPaymentEvent();
|
||||
$this->currentPleb->load('paymentEvents');
|
||||
}
|
||||
$this->loadEvents();
|
||||
$this->listenForPayment();
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->with([
|
||||
'paymentEvents' => fn($query)
|
||||
=> $query->where('year', date('Y')),
|
||||
])
|
||||
->where('pubkey', $pubkey)->first();
|
||||
$this->email = $this->currentPleb->email;
|
||||
$this->no = $this->currentPleb->no_email;
|
||||
$this->showEmail = !$this->no;
|
||||
if ($this->currentPleb->association_status === \App\Enums\AssociationStatus::ACTIVE) {
|
||||
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
|
||||
}
|
||||
if ($this->currentPleb->paymentEvents->count() < 1) {
|
||||
$this->createPaymentEvent();
|
||||
$this->currentPleb->load('paymentEvents');
|
||||
}
|
||||
$this->loadEvents();
|
||||
$this->listenForPayment();
|
||||
},
|
||||
'nostrLoggedOut' => function () {
|
||||
\App\Support\NostrAuth::logout();
|
||||
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
$this->yearsPaid = [];
|
||||
$this->events = [];
|
||||
$this->payments = [];
|
||||
$this->qrCode = null;
|
||||
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
|
||||
$this->currentYearIsPaid = false;
|
||||
},
|
||||
]);
|
||||
|
||||
updated([
|
||||
'no' => function () {
|
||||
$this->showEmail = !$this->no;
|
||||
$this->currentPleb->update([
|
||||
'no_email' => $this->no,
|
||||
]);
|
||||
},
|
||||
'fax' => function () {
|
||||
$this->js('alert("Markus Turm wird sich per Fax melden!")');
|
||||
},
|
||||
]);
|
||||
|
||||
$saveEmail = function () {
|
||||
$this->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
$this->currentPleb->update([
|
||||
'email' => $this->email,
|
||||
]);
|
||||
$notification = new Notification($this);
|
||||
$notification->success('E-Mail Adresse gespeichert.');
|
||||
};
|
||||
|
||||
$pay = function ($comment) {
|
||||
$paymentEvent = $this->currentPleb
|
||||
->paymentEvents()
|
||||
->where('year', date('Y'))
|
||||
->first();
|
||||
if ($paymentEvent->btc_pay_invoice) {
|
||||
return redirect('https://pay.einundzwanzig.space/i/'.$paymentEvent->btc_pay_invoice);
|
||||
}
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'token '.config('services.btc_pay.api_key'),
|
||||
])->post(
|
||||
'https://pay.einundzwanzig.space/api/v1/stores/98PF86BoMd3C8P1nHHyFdoeznCwtcm5yehcAgoCYDQ2a/invoices',
|
||||
[
|
||||
'amount' => $this->amountToPay,
|
||||
'metadata' => [
|
||||
'orderId' => $comment,
|
||||
'orderUrl' => url()->route('association.profile'),
|
||||
'itemDesc' => 'Mitgliedsbeitrag '.date('Y').' von nostr:'.$this->currentPleb->npub,
|
||||
'posData' => [
|
||||
'event' => $paymentEvent->event_id,
|
||||
'pubkey' => $this->currentPleb->pubkey,
|
||||
'npub' => $this->currentPleb->npub,
|
||||
],
|
||||
],
|
||||
'checkout' => [
|
||||
'expirationMinutes' => 60 * 24,
|
||||
'redirectURL' => url()->route('association.profile'),
|
||||
'redirectAutomatically' => true,
|
||||
'defaultLanguage' => 'de',
|
||||
],
|
||||
],
|
||||
)->throw();
|
||||
$paymentEvent->btc_pay_invoice = $response->json()['id'];
|
||||
$paymentEvent->save();
|
||||
|
||||
return redirect($response->json()['checkoutLink']);
|
||||
} catch (Exception $e) {
|
||||
$notification = new Notification($this);
|
||||
$notification->error(
|
||||
'Fehler beim Erstellen der Rechnung. Bitte versuche es später erneut: '.$e->getMessage(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$listenForPayment = function () {
|
||||
$paymentEvent = $this->currentPleb
|
||||
->paymentEvents()
|
||||
->where('year', date('Y'))
|
||||
->first();
|
||||
if ($paymentEvent->btc_pay_invoice) {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'token '.config('services.btc_pay.api_key'),
|
||||
])
|
||||
->get(
|
||||
'https://pay.einundzwanzig.space/api/v1/stores/98PF86BoMd3C8P1nHHyFdoeznCwtcm5yehcAgoCYDQ2a/invoices/'.$paymentEvent->btc_pay_invoice,
|
||||
);
|
||||
if ($response->json()['status'] === 'Expired') {
|
||||
$paymentEvent->btc_pay_invoice = null;
|
||||
$paymentEvent->paid = false;
|
||||
$paymentEvent->save();
|
||||
}
|
||||
if ($response->json()['status'] === 'Settled') {
|
||||
$paymentEvent->paid = true;
|
||||
$paymentEvent->save();
|
||||
$this->currentYearIsPaid = true;
|
||||
}
|
||||
}
|
||||
if ($paymentEvent->paid) {
|
||||
$this->currentYearIsPaid = true;
|
||||
}
|
||||
$paymentEvent = $paymentEvent->refresh();
|
||||
$this->payments = $this->currentPleb
|
||||
->paymentEvents()
|
||||
->where('paid', true)
|
||||
->get();
|
||||
};
|
||||
|
||||
$save = function ($type) {
|
||||
$this->form->validate();
|
||||
if (!$this->form->check) {
|
||||
$this->js('alert("Du musst den Statuten zustimmen.")');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->currentPleb
|
||||
->update([
|
||||
'association_status' => $type,
|
||||
]);
|
||||
};
|
||||
|
||||
$createPaymentEvent = function () {
|
||||
$note = new NostrEvent();
|
||||
$note->setKind(32121);
|
||||
$note->setContent(
|
||||
'Dieses Event dient der Zahlung des Mitgliedsbeitrags für das Jahr '.date(
|
||||
'Y',
|
||||
).'. Bitte bezahle den Betrag von '.number_format($this->amountToPay, 0, ',', '.').' Satoshis.',
|
||||
);
|
||||
$note->setTags([
|
||||
['d', $this->currentPleb->pubkey.','.date('Y')],
|
||||
['zap', 'daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6', config('services.relay'), '1'],
|
||||
]);
|
||||
$signer = new Sign();
|
||||
$signer->signEvent($note, config('services.nostr'));
|
||||
|
||||
$eventMessage = new EventMessage($note);
|
||||
|
||||
$relayUrl = config('services.relay');
|
||||
$relay = new Relay($relayUrl);
|
||||
$relay->setMessage($eventMessage);
|
||||
$result = $relay->send();
|
||||
|
||||
$this->currentPleb->paymentEvents()->create([
|
||||
'year' => date('Y'),
|
||||
'event_id' => $result->eventId,
|
||||
'amount' => $this->amountToPay,
|
||||
]);
|
||||
};
|
||||
|
||||
$loadEvents = function () {
|
||||
$subscription = new Subscription();
|
||||
$subscriptionId = $subscription->setId();
|
||||
|
||||
$filter1 = new Filter();
|
||||
$filter1->setKinds([32121]);
|
||||
$filter1->setAuthors(['daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6']);
|
||||
$filters = [$filter1];
|
||||
|
||||
$requestMessage = new RequestMessage($subscriptionId, $filters);
|
||||
|
||||
$relays = [
|
||||
new Relay(config('services.relay')),
|
||||
];
|
||||
$relaySet = new RelaySet();
|
||||
$relaySet->setRelays($relays);
|
||||
|
||||
$request = new Request($relaySet, $requestMessage);
|
||||
$response = $request->send();
|
||||
|
||||
$this->events = collect($response[config('services.relay')])
|
||||
->map(function ($event) {
|
||||
if (!isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->unique('id')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app
|
||||
:seo="new \RalphJSmit\Laravel\SEO\Support\SEOData(title: 'Mitgliedschaft', description: 'Einundzwanzig ist, was du draus machst.')"
|
||||
>
|
||||
@volt
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
|
||||
<!-- Page header -->
|
||||
@@ -314,39 +33,8 @@ $loadEvents = function () {
|
||||
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 -->
|
||||
@@ -466,7 +154,6 @@ $loadEvents = function () {
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap space-y-2 sm:space-y-0 items-center justify-between">
|
||||
{{-- https://v.nostr.build/bomfuwLnOTIDrP4y.mp4 --}}
|
||||
<template x-if="$store.nostr.user">
|
||||
<div class="flex items">
|
||||
<img class="w-12 h-12 rounded-full"
|
||||
@@ -499,31 +186,6 @@ $loadEvents = function () {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{--<section>
|
||||
@if($currentPubkey && !$currentPleb->application_for && $currentPleb->association_status->value < 2)
|
||||
<h3 class="text-xl leading-snug text-[#1B1B1B] dark:text-gray-100 font-bold mb-1">
|
||||
passives Mitglied werden
|
||||
</h3>
|
||||
<h4 class="text-xs leading-snug text-[#1B1B1B] dark:text-gray-100 font-italic mb-1">
|
||||
Passivmitglieder haben kein Stimmrecht. Firmen können nur Passivmitglieder werden und zahlen das 100-fache des festgelegten Beitrags.
|
||||
</h4>
|
||||
<div class="text-sm">
|
||||
<x-textarea
|
||||
corner="Beschreibe deine Motivation, passives Mitglied zu werden."
|
||||
label="Warum möchtest du passives Mitglied werden?"
|
||||
wire:model="form.reason"/>
|
||||
</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 flex flex-col space-y-2">
|
||||
<x-button label="Für passive Mitgliedschaft bewerben"
|
||||
wire:click="save({{ \App\Enums\AssociationStatus::PASSIVE() }})"/>
|
||||
<x-badge outline
|
||||
label="Es wird im Anschluss ein Nostr Event erzeugt, das du mit dem Mitgliedsbeitrag zappen kannst, nachdem du bestätigt wurdest."/>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>--}}
|
||||
|
||||
<section>
|
||||
@if($currentPubkey && !$currentPleb->application_for && $currentPleb->association_status->value < 2)
|
||||
<h3 class="text-xl leading-snug text-[#1B1B1B] dark:text-gray-100 font-bold mb-1">
|
||||
@@ -547,7 +209,7 @@ $loadEvents = function () {
|
||||
</div>
|
||||
</div>
|
||||
<x-button label="Mit deinem aktuellen Nostr-Profil Mitglied werden"
|
||||
wire:click="save({{ \App\Enums\AssociationStatus::PASSIVE() }})"/>
|
||||
wire:click="save({{ AssociationStatus::PASSIVE() }})"/>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -602,35 +264,6 @@ $loadEvents = function () {
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@if($currentPubkey && $currentPleb->application_for)
|
||||
<div
|
||||
class="inline-flex flex-col w-full max-w-lg px-4 py-2 rounded-lg text-sm bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-400">
|
||||
<div class="flex w-full justify-between items-start">
|
||||
<div class="flex">
|
||||
<svg class="shrink-0 fill-current text-yellow-500 mt-[3px] mr-3" width="16"
|
||||
height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">
|
||||
Du hast dich erfolgreich mit folgendem Grund beworben:
|
||||
</div>
|
||||
<div>{{ $currentPleb->application_text }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">
|
||||
Schaue später vorbei, denn nun muss jemand aus dem Vorstand deine
|
||||
Bewerbung prüfen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@if($currentPleb && $currentPleb->association_status->value > 1)
|
||||
<div class="flex flex-col space-y-4">
|
||||
@@ -790,5 +423,4 @@ $loadEvents = function () {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,77 +1,6 @@
|
||||
<?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, 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();
|
||||
|
||||
mount(function () {
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
},
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
$save = function () {
|
||||
$this->form->validate();
|
||||
|
||||
$projectProposal = \App\Models\ProjectProposal::query()->create([
|
||||
...$this->form->all(),
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
]);
|
||||
if ($this->image) {
|
||||
$this->validate([
|
||||
'image' => 'image|max:1024',
|
||||
]);
|
||||
$projectProposal
|
||||
->addMedia($this->image->getRealPath())
|
||||
->toMediaCollection('main');
|
||||
}
|
||||
|
||||
return redirect()->route('association.projectSupport');
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="Neuer Vorschlag für eine Unterstützung">
|
||||
@volt
|
||||
<div>
|
||||
@if($isAllowed)
|
||||
<?php if($isAllowed): ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<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">
|
||||
@@ -79,18 +8,27 @@ $save = function () {
|
||||
|
||||
<x-input.group :for=" md5('image')" :label="__('Bild')">
|
||||
<div class="py-4">
|
||||
@if ($image && method_exists($image, 'temporaryUrl') && str($image->getMimeType())->contains(['image/jpeg','image/jpg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp']))
|
||||
<?php if ($image && method_exists($image, 'temporaryUrl') && 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'))
|
||||
<img class="h-48 object-contain" src="<?php echo e($image->temporaryUrl()); ?>">
|
||||
<?php endif; ?>
|
||||
<?php 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
|
||||
src="<?php echo e($projectProposal->getFirstMediaUrl('main')); ?>">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input class="text-gray-200" type="file" wire:model="image">
|
||||
@error('image') <span class="text-red-500">{{ $message }}</span> @enderror
|
||||
<?php $__errorArgs = ['image'];
|
||||
$__bag = $errors->getBag($__errorProps ?? 'default');
|
||||
if ($__bag->has($__errorArgs)) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs); ?>
|
||||
<span class="text-red-500"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</x-input.group>
|
||||
|
||||
<x-input.group :for="md5('form.name')" :label="__('Name')">
|
||||
@@ -113,33 +51,41 @@ $save = function () {
|
||||
<x-input.group :for="md5('form.description')">
|
||||
<x-slot name="label">
|
||||
<div>
|
||||
{{ __('Beschreibung') }}
|
||||
<?php echo e(__('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>
|
||||
class="text-amber-500 text-xs py-2"><?php echo e(__('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>
|
||||
class="text-amber-500 text-xs py-2"><?php echo e(__('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
|
||||
<?php $__errorArgs = ['form.description'];
|
||||
$__bag = $errors->getBag($__errorProps ?? 'default');
|
||||
if ($__bag->has($__errorArgs)) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs); ?>
|
||||
<span class="text-red-500 py-2"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</x-input.group>
|
||||
|
||||
<x-input.group :for="md5('save')" label="">
|
||||
<x-button secondary :href="route('association.projectSupport')">
|
||||
<i class="fa fa-thin fa-arrow-left"></i>
|
||||
{{ __('Zurück') }}
|
||||
<?php echo e(__('Zurück')); ?>
|
||||
</x-button>
|
||||
<x-button primary wire:click="save">
|
||||
<i class="fa fa-thin fa-save"></i>
|
||||
{{ __('Save') }}
|
||||
<?php echo e(__('Save')); ?>
|
||||
</x-button>
|
||||
</x-input.group>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
@@ -151,7 +97,6 @@ $save = function () {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,81 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\ProjectProposalForm;
|
||||
use App\Models\ProjectProposal;
|
||||
use App\Support\NostrAuth;
|
||||
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, name};
|
||||
use function Livewire\Volt\{state, mount, on, computed, form, usesFileUploads};
|
||||
|
||||
name('association.projectSupport.edit');
|
||||
|
||||
form(ProjectProposalForm::class);
|
||||
|
||||
state([
|
||||
'projectProposal' => fn() => $projectProposal,
|
||||
'image',
|
||||
'isAllowed' => false,
|
||||
'currentPubkey' => null,
|
||||
'currentPleb' => null,
|
||||
]);
|
||||
|
||||
usesFileUploads();
|
||||
|
||||
mount(function (ProjectProposal $projectProposal) {
|
||||
if (NostrAuth::check()) {
|
||||
$this->currentPubkey = NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
$this->form->fill($projectProposal->toArray());
|
||||
$this->image = $projectProposal->getFirstMedia('main');
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
},
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
$save = function () {
|
||||
$this->form->validate();
|
||||
if ($this->image && method_exists($this->image, 'temporaryUrl')) {
|
||||
$this->validate([
|
||||
'image' => 'nullable|image|max:1024',
|
||||
]);
|
||||
$this->projectProposal
|
||||
->addMedia($this->image->getRealPath())
|
||||
->toMediaCollection('main');
|
||||
}
|
||||
|
||||
$this->projectProposal->update([
|
||||
...$this->form->except('id', 'slug'),
|
||||
]);
|
||||
|
||||
return redirect()->route('association.projectSupport');
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="{{ $projectProposal->name }}">
|
||||
@volt
|
||||
<x-layouts.app title="<?php echo e($projectProposal->name); ?>">
|
||||
<div>
|
||||
@if($isAllowed)
|
||||
<?php if($isAllowed): ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<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">
|
||||
@@ -83,18 +8,27 @@ $save = function () {
|
||||
|
||||
<x-input.group :for=" md5('image')" :label="__('Bild')">
|
||||
<div class="py-4">
|
||||
@if ($image && method_exists($image, 'temporaryUrl') && str($image->getMimeType())->contains(['image/jpeg','image/jpg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp']))
|
||||
<?php if ($image && method_exists($image, 'temporaryUrl') && 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'))
|
||||
<img class="h-48 object-contain" src="<?php echo e($image->temporaryUrl()); ?>">
|
||||
<?php endif; ?>
|
||||
<?php 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
|
||||
src="<?php echo e($projectProposal->getFirstMediaUrl('main')); ?>">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input class="text-gray-200" type="file" wire:model="image">
|
||||
@error('image') <span class="text-red-500">{{ $message }}</span> @enderror
|
||||
<?php $__errorArgs = ['image'];
|
||||
$__bag = $errors->getBag($__errorProps ?? 'default');
|
||||
if ($__bag->has($__errorArgs)) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs); ?>
|
||||
<span class="text-red-500"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</x-input.group>
|
||||
|
||||
<x-input.group :for="md5('form.name')" :label="__('Name')">
|
||||
@@ -126,33 +60,41 @@ $save = function () {
|
||||
<x-input.group :for="md5('form.description')">
|
||||
<x-slot name="label">
|
||||
<div>
|
||||
{{ __('Beschreibung') }}
|
||||
<?php echo e(__('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>
|
||||
class="text-amber-500 text-xs py-2"><?php echo e(__('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>
|
||||
class="text-amber-500 text-xs py-2"><?php echo e(__('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
|
||||
<?php $__errorArgs = ['form.description'];
|
||||
$__bag = $errors->getBag($__errorProps ?? 'default');
|
||||
if ($__bag->has($__errorArgs)) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs); ?>
|
||||
<span class="text-red-500 py-2"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</x-input.group>
|
||||
|
||||
<x-input.group :for="md5('save')" label="">
|
||||
<x-button secondary :href="route('association.projectSupport')">
|
||||
<i class="fa fa-thin fa-arrow-left"></i>
|
||||
{{ __('Zurück') }}
|
||||
<?php echo e(__('Zurück')); ?>
|
||||
</x-button>
|
||||
<x-button primary wire:click="save">
|
||||
<i class="fa fa-thin fa-save"></i>
|
||||
{{ __('Speichern') }}
|
||||
<?php echo e(__('Speichern')); ?>
|
||||
</x-button>
|
||||
</x-input.group>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
@@ -164,7 +106,6 @@ $save = function () {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,75 @@
|
||||
<x-layouts.app
|
||||
:seo="new \RalphJSmit\Laravel\SEO\Support\SEOData(title: 'Projekt Unterstützungen', description: 'Einundzwanzig Projektunterstützungen')"
|
||||
>
|
||||
<div>
|
||||
<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-cols-1 sm:grid-cols-2 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 -->
|
||||
@if($currentPleb && $currentPleb->association_status->value > 1 && $currentPleb->paymentEvents()->where('year', date('Y'))->where('paid', true)->exists())
|
||||
<x-button :href="route('association.projectSupport.create')" icon="plus"
|
||||
label="Projekt einreichen"/>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-5">
|
||||
<ul class="flex flex-wrap -m-1">
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('all')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border @if($activeFilter === 'all') border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800 @else 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 @endif transition">
|
||||
Alle
|
||||
</button>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('new')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border @if($activeFilter === 'new') border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800 @else 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 @endif transition">
|
||||
Neu
|
||||
</button>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('supported')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border @if($activeFilter === 'supported') border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800 @else 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 @endif transition">
|
||||
Unterstützt
|
||||
</button>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('rejected')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border @if($activeFilter === 'rejected') border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800 @else 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 @endif transition">
|
||||
Abgelehnt
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 italic mb-4">{{ $projects->count() }} Projekte</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="grid xl:grid-cols-2 gap-6 mb-8">
|
||||
@foreach($this->projects as $project)
|
||||
<x-project-card :project="$project" :currentPleb="$currentPleb" :section="$activeFilter"/>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -1,114 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\VoteForm;
|
||||
use App\Models\Vote;
|
||||
use Livewire\Volt\Component;
|
||||
use RalphJSmit\Laravel\SEO\Support\SEOData;
|
||||
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, name};
|
||||
use function Livewire\Volt\{state, mount, on, computed, form, with};
|
||||
|
||||
name('association.projectSupport.item');
|
||||
|
||||
form(VoteForm::class);
|
||||
|
||||
state([
|
||||
'projectProposal' => fn() => $projectProposal,
|
||||
'isAllowed' => false,
|
||||
'currentPubkey' => null,
|
||||
'currentPleb' => null,
|
||||
'ownVoteExists' => false,
|
||||
'boardVotes' => fn() => $this->getBoardVotes(),
|
||||
'otherVotes' => fn() => $this->getOtherVotes(),
|
||||
]);
|
||||
|
||||
mount(function () {
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$this->handleNostrLoggedIn($this->currentPubkey);
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => fn($pubkey) => $this->handleNostrLoggedIn($pubkey),
|
||||
'nostrLoggedOut' => fn() => $this->handleNostrLoggedOut(),
|
||||
]);
|
||||
|
||||
$approve = fn() => $this->handleApprove();
|
||||
$notApprove = fn() => $this->handleNotApprove();
|
||||
|
||||
$getBoardVotes = function () {
|
||||
return Vote::query()
|
||||
->where('project_proposal_id', $this->projectProposal->id)
|
||||
->whereHas('einundzwanzigPleb', fn($q) => $q->whereIn('npub', config('einundzwanzig.config.current_board')))
|
||||
->get();
|
||||
};
|
||||
|
||||
$getOtherVotes = function () {
|
||||
return Vote::query()
|
||||
->where('project_proposal_id', $this->projectProposal->id)
|
||||
->whereDoesntHave(
|
||||
'einundzwanzigPleb',
|
||||
fn($q) => $q->whereIn('npub', config('einundzwanzig.config.current_board'))
|
||||
)
|
||||
->get();
|
||||
};
|
||||
|
||||
$handleNostrLoggedIn = function ($pubkey) {
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
$this->ownVoteExists = Vote::query()
|
||||
->where('project_proposal_id', $this->projectProposal->id)
|
||||
->where('einundzwanzig_pleb_id', $this->currentPleb->id)
|
||||
->exists();
|
||||
};
|
||||
|
||||
$handleNostrLoggedOut = function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
};
|
||||
|
||||
$handleApprove = function () {
|
||||
Vote::query()->updateOrCreate([
|
||||
'project_proposal_id' => $this->projectProposal->id,
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
], [
|
||||
'value' => true,
|
||||
]);
|
||||
$this->form->reset();
|
||||
$this->ownVoteExists = true;
|
||||
$this->boardVotes = $this->getBoardVotes();
|
||||
$this->otherVotes = $this->getOtherVotes();
|
||||
};
|
||||
|
||||
$handleNotApprove = function () {
|
||||
$this->form->validate();
|
||||
|
||||
Vote::query()->updateOrCreate([
|
||||
'project_proposal_id' => $this->projectProposal->id,
|
||||
'einundzwanzig_pleb_id' => $this->currentPleb->id,
|
||||
], [
|
||||
'value' => false,
|
||||
]);
|
||||
$this->form->reset();
|
||||
$this->ownVoteExists = true;
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app
|
||||
:seo="new SEOData(title: 'Unterstützung für: ' . $projectProposal->name, description: $projectProposal->accepted ? 'Wurde mit ' . number_format($projectProposal->sats_paid, 0, ',', '.') . ' Satoshis unterstützt!' :str($projectProposal->description)->limit(100, '...', true), image: $projectProposal->getFirstMediaUrl('main'))">
|
||||
@volt
|
||||
<div>
|
||||
@if($projectProposal->accepted || $isAllowed)
|
||||
<?php if($projectProposal->accepted || $isAllowed): ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full">
|
||||
|
||||
<!-- Page content -->
|
||||
@@ -118,7 +11,7 @@ $handleNotApprove = function () {
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<a class="btn-sm px-3 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 text-gray-800 dark:text-gray-300"
|
||||
href="{{ route('association.projectSupport') }}"
|
||||
href="<?php echo e(route('association.projectSupport')); ?>"
|
||||
>
|
||||
<svg class="fill-current text-gray-400 dark:text-gray-500 mr-2" width="7" height="12"
|
||||
viewBox="0 0 7 12">
|
||||
@@ -128,15 +21,15 @@ $handleNotApprove = function () {
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-violet-500 uppercase mb-2">
|
||||
{{ $projectProposal->created_at->translatedFormat('d.m.Y') }}
|
||||
<?php echo e($projectProposal->created_at->translatedFormat('d.m.Y')); ?>
|
||||
</div>
|
||||
<header class="mb-4">
|
||||
<!-- Title -->
|
||||
<h1 class="text-2xl md:text-3xl text-gray-800 dark:text-gray-100 font-bold mb-2">
|
||||
{{ $projectProposal->name }}
|
||||
<?php echo e($projectProposal->name); ?>
|
||||
</h1>
|
||||
<x-markdown>
|
||||
{!! $projectProposal->description !!}
|
||||
<?php echo $projectProposal->description; ?>
|
||||
</x-markdown>
|
||||
</header>
|
||||
|
||||
@@ -145,12 +38,12 @@ $handleNotApprove = function () {
|
||||
<div class="flex items-center sm:mr-4">
|
||||
<a class="block mr-2 shrink-0" href="#0">
|
||||
<img class="rounded-full"
|
||||
src="{{ $projectProposal->einundzwanzigPleb->profile?->picture ?? asset('einundzwanzig-alpha.jpg') }}"
|
||||
src="<?php echo e($projectProposal->einundzwanzigPleb->profile?->picture ?? asset('einundzwanzig-alpha.jpg')); ?>"
|
||||
width="32" height="32" alt="User 04">
|
||||
</a>
|
||||
<div class="text-sm whitespace-nowrap">Eingereicht von
|
||||
<div
|
||||
class="font-semibold text-gray-800 dark:text-gray-100">{{ $projectProposal->einundzwanzigPleb?->profile->name ?? str($projectProposal->einundzwanzigPleb->npub)->limit(32) }}</div>
|
||||
class="font-semibold text-gray-800 dark:text-gray-100"><?php echo e($projectProposal->einundzwanzigPleb?->profile->name ?? str($projectProposal->einundzwanzigPleb->npub)->limit(32)); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right side -->
|
||||
@@ -158,56 +51,31 @@ $handleNotApprove = function () {
|
||||
<!-- Tags -->
|
||||
<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">
|
||||
<a target="_blank" href="{{ $projectProposal->website }}"><span>Webseite</span></a>
|
||||
<a target="_blank" href="<?php echo e($projectProposal->website); ?>"><span>Webseite</span></a>
|
||||
</div>
|
||||
<div
|
||||
class="text-xs inline-flex font-medium uppercase bg-green-500/20 text-green-700 rounded-full text-center px-2.5 py-1">
|
||||
{{ number_format($projectProposal->support_in_sats, 0, ',', '.') }} Sats
|
||||
<?php echo e(number_format($projectProposal->support_in_sats, 0, ',', '.')); ?> Sats
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<figure class="mb-6">
|
||||
<img class="rounded-sm h-48" src="{{ $projectProposal->getFirstMediaUrl('main') }}"
|
||||
<img class="rounded-sm h-48" src="<?php echo e($projectProposal->getFirstMediaUrl('main')); ?>"
|
||||
alt="Picture">
|
||||
</figure>
|
||||
|
||||
<hr class="my-6 border-t border-gray-100 dark:border-gray-700/60">
|
||||
|
||||
<!-- Reasons -->
|
||||
{{--<div>
|
||||
<h2 class="text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-2">
|
||||
Ablehnungen ({{ count($reasons) }})
|
||||
</h2>
|
||||
<ul class="space-y-5 my-6">
|
||||
@foreach($reasons as $reason)
|
||||
<li class="flex items-start">
|
||||
<a class="block mr-3 shrink-0" href="#0">
|
||||
<img class="rounded-full"
|
||||
src="{{ $reason->einundzwanzigPleb->profile->picture }}"
|
||||
width="32" height="32"
|
||||
alt="{{ $reason->einundzwanzigPleb->profile->name }}">
|
||||
</a>
|
||||
<div class="grow">
|
||||
<div class="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-2">
|
||||
{{ $reason->einundzwanzigPleb->profile->name }}
|
||||
</div>
|
||||
<div class="italic">{{ $reason->reason }}</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>--}}
|
||||
|
||||
</div>
|
||||
|
||||
@if($isAllowed && !$projectProposal->accepted)
|
||||
<?php if($isAllowed && !$projectProposal->accepted): ?>
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- 1st block -->
|
||||
<div class="bg-white dark:bg-gray-800 p-5 shadow-sm rounded-xl lg:w-72 xl:w-80">
|
||||
@if(!$ownVoteExists)
|
||||
<?php if(!$ownVoteExists): ?>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
wire:click="approve"
|
||||
@@ -221,20 +89,19 @@ $handleNotApprove = function () {
|
||||
<i class="fill-current shrink-0 fa-sharp-duotone fa-solid fa-thumbs-down"></i>
|
||||
<span class="ml-1">Ablehnen</span>
|
||||
</button>
|
||||
{{--<x-textarea wire:model="form.reason" label="Grund für deine Ablehnung"/>--}}
|
||||
</div>
|
||||
@else
|
||||
<?php else: ?>
|
||||
<div class="space-y-2">
|
||||
<p>Du hast bereits abgestimmt.</p>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 2nd block -->
|
||||
<div class="bg-white dark:bg-gray-800 p-5 shadow-sm rounded-xl lg:w-72 xl:w-80">
|
||||
<div class="flex justify-between space-x-1 mb-5">
|
||||
<div class="text-sm text-gray-800 dark:text-gray-100 font-semibold">
|
||||
Zustimmungen des Vorstands ({{ count($boardVotes->where('value', 1)) }})
|
||||
Zustimmungen des Vorstands (<?php echo e(count($boardVotes->where('value', 1))); ?>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +110,7 @@ $handleNotApprove = function () {
|
||||
<div class="bg-white dark:bg-gray-800 p-5 shadow-sm rounded-xl lg:w-72 xl:w-80">
|
||||
<div class="flex justify-between space-x-1 mb-5">
|
||||
<div class="text-sm text-gray-800 dark:text-gray-100 font-semibold">
|
||||
Ablehnungen des Vorstands ({{ count($boardVotes->where('value', 0)) }})
|
||||
Ablehnungen des Vorstands (<?php echo e(count($boardVotes->where('value', 0))); ?>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,8 +119,7 @@ $handleNotApprove = function () {
|
||||
<div class="bg-white dark:bg-gray-800 p-5 shadow-sm rounded-xl lg:w-72 xl:w-80">
|
||||
<div class="flex justify-between space-x-1 mb-5">
|
||||
<div class="text-sm text-gray-800 dark:text-gray-100 font-semibold">
|
||||
Zustimmungen der übrigen Mitglieder ({{ count($otherVotes->where('value', 1)) }}
|
||||
)
|
||||
Zustimmungen der übrigen Mitglieder (<?php echo e(count($otherVotes->where('value', 1))); ?>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,18 +128,18 @@ $handleNotApprove = function () {
|
||||
<div class="bg-white dark:bg-gray-800 p-5 shadow-sm rounded-xl lg:w-72 xl:w-80">
|
||||
<div class="flex justify-between space-x-1 mb-5">
|
||||
<div class="text-sm text-gray-800 dark:text-gray-100 font-semibold">
|
||||
Ablehnungen der übrigen Mitglieder ({{ count($otherVotes->where('value', 0)) }})
|
||||
Ablehnungen der übrigen Mitglieder (<?php echo e(count($otherVotes->where('value', 0))); ?>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<?php else: ?>
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
@@ -286,7 +152,6 @@ $handleNotApprove = function () {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,43 +1,4 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
use function Livewire\Volt\{
|
||||
computed,
|
||||
mount,
|
||||
state,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
name('changelog');
|
||||
|
||||
state(['entries' => []]);
|
||||
|
||||
mount(function () {
|
||||
$output = shell_exec('git log -n1000 --pretty=format:"%H|%s|%an|%ad" --date=format:"%Y-%m-%d %H:%M:%S"');
|
||||
$lines = explode("\n", trim($output));
|
||||
$entries = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
[$hash, $message, $author, $date] = explode('|', $line);
|
||||
$entries[] = [
|
||||
'hash' => $hash,
|
||||
'message' => $message,
|
||||
'author' => $author,
|
||||
'date' => $date,
|
||||
];
|
||||
}
|
||||
$this->entries = $entries;
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="{{ __('Changelog') }}">
|
||||
@volt
|
||||
<div>
|
||||
<div
|
||||
class="sm:flex sm:justify-between sm:items-center px-4 sm:px-6 py-8 border-b border-gray-200 dark:border-gray-700/60">
|
||||
@@ -103,5 +64,4 @@ mount(function () {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,58 +1,4 @@
|
||||
<?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,
|
||||
mount,
|
||||
state,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
name('einundzwanzig-feed');
|
||||
|
||||
state(
|
||||
['events' => []],
|
||||
);
|
||||
|
||||
state(
|
||||
['newEvents' => false],
|
||||
);
|
||||
|
||||
mount(function () {
|
||||
$this->events = \App\Models\Event::query()
|
||||
->where('type', 'root')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with([
|
||||
'renderedEvent',
|
||||
])
|
||||
->get()
|
||||
->toArray();
|
||||
});
|
||||
|
||||
on(['echo:events,.newEvents' => function () {
|
||||
$this->newEvents = true;
|
||||
}]);
|
||||
|
||||
$loadMore = function () {
|
||||
// Load more events
|
||||
$this->newEvents = false;
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="Einundzwanzig Feed">
|
||||
@volt
|
||||
<div class="px-8 py-8 space-y-6">
|
||||
<div>
|
||||
@if($newEvents)
|
||||
@@ -62,7 +8,7 @@ $loadMore = function () {
|
||||
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
d="M18 10a8 8 0 11-16 0 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -94,7 +40,6 @@ $loadMore = function () {
|
||||
{{ $event['rendered_event']['profile_name'] }}
|
||||
</div>
|
||||
</div>
|
||||
{{--<div class="text-gray-600">{{ $event['event_id'] }}</div>--}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-4 text-xs">
|
||||
@@ -114,5 +59,4 @@ $loadMore = function () {
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,32 +1,4 @@
|
||||
<?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,
|
||||
mount,
|
||||
state,
|
||||
with,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
name('meetups.grid');
|
||||
|
||||
?>
|
||||
|
||||
|
||||
<x-layouts.app title="{{ __('Meetups') }}">
|
||||
@volt
|
||||
<div class="relative flex">
|
||||
|
||||
<!-- Profile sidebar -->
|
||||
@@ -247,5 +219,4 @@ name('meetups.grid');
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
35
resources/views/livewire/meetups/mockup.blade.php
Normal file
35
resources/views/livewire/meetups/mockup.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<x-layouts.app title="{{ __('Mockup') }}">
|
||||
<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>
|
||||
</x-layouts.app>
|
||||
5
resources/views/livewire/meetups/table.blade.php
Normal file
5
resources/views/livewire/meetups/table.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<x-layouts.app title="{{ __('Meetups') }}">
|
||||
<div>
|
||||
<livewire:meetup-table />
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
46
resources/views/livewire/meetups/worldmap.blade.php
Normal file
46
resources/views/livewire/meetups/worldmap.blade.php
Normal file
@@ -0,0 +1,46 @@
|
||||
@push('scripts')
|
||||
<script src="{{ asset('dist/jquery.js') }}"></script>
|
||||
<script src="{{ asset('vendor/jvector/jquery-jvectormap-2.0.5.min.js') }}"></script>
|
||||
<script src="{{ asset('vendor/jvector/maps/world-mill.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ asset('vendor/jvector/jquery-jvectormap-2.0.5.css') }}" type="text/css"
|
||||
media="screen"/>
|
||||
@endpush
|
||||
|
||||
<x-layouts.app title="{{ __('Worldmap') }}">
|
||||
<div
|
||||
wire:ignore
|
||||
class="w-full flex justify-center"
|
||||
x-data="{
|
||||
init() {
|
||||
let markers = {{ Js::from($markers) }};
|
||||
|
||||
$('#mapworld').vectorMap({
|
||||
zoomButtons : true,
|
||||
zoomOnScroll: true,
|
||||
map: 'world_mill',
|
||||
backgroundColor: 'transparent',
|
||||
markers: markers.map(function(h){ return {name: h.name, latLng: h.coords} }),
|
||||
onMarkerClick: function(event, index) {
|
||||
$wire.call('filterByMarker', markers[index].id)
|
||||
},
|
||||
markerStyle: {
|
||||
initial: {
|
||||
image: '{{ asset('img/btc.png') }}',
|
||||
}
|
||||
},
|
||||
regionStyle: {
|
||||
initial: {
|
||||
fill: '#a4a4a4'
|
||||
},
|
||||
hover: {
|
||||
'fill-opacity': 1,
|
||||
cursor: 'default'
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}"
|
||||
>
|
||||
<div id="mapworld" style="width: 100%;" class="h-[200px] sm:h-[400px]"></div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
5
resources/views/livewire/welcome.blade.php
Normal file
5
resources/views/livewire/welcome.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<x-layouts.app title="Welcome">
|
||||
<div>
|
||||
TEST
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -1,257 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
use swentel\nostr\{Filter\Filter,
|
||||
Key\Key,
|
||||
Message\EventMessage,
|
||||
Message\RequestMessage,
|
||||
Relay\Relay,
|
||||
Relay\RelaySet,
|
||||
Request\Request,
|
||||
Subscription\Subscription,
|
||||
Event\Event as NostrEvent,
|
||||
Sign\Sign};
|
||||
|
||||
use function Livewire\Volt\{computed, mount, state, with, updated, on};
|
||||
use function Laravel\Folio\{middleware, name};
|
||||
|
||||
name('association.election.admin');
|
||||
|
||||
state([
|
||||
'isAllowed' => false,
|
||||
'currentPubkey' => null,
|
||||
'votes' => null,
|
||||
'boardVotes' => null,
|
||||
'events' => null,
|
||||
'boardEvents' => null,
|
||||
'election' => fn() => $election,
|
||||
'signThisEvent' => '',
|
||||
'plebs' => fn()
|
||||
=> \App\Models\EinundzwanzigPleb::query()
|
||||
->with(['profile'])
|
||||
->whereIn('association_status', [3, 4])
|
||||
->orderBy('association_status', 'desc')
|
||||
->get()
|
||||
->toArray(),
|
||||
'electionConfig' => fn()
|
||||
=> collect(json_decode($this->election->candidates, true, 512, JSON_THROW_ON_ERROR))
|
||||
->map(fn($c)
|
||||
=> [
|
||||
'type' => $c['type'],
|
||||
'c' => $c['c'],
|
||||
'candidates' => \App\Models\Profile::query()
|
||||
->whereIn('pubkey', $c['c'])
|
||||
->get()
|
||||
->map(fn($p)
|
||||
=> [
|
||||
'pubkey' => $p->pubkey,
|
||||
'name' => $p->name,
|
||||
'picture' => $p->picture,
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
mount(fn()
|
||||
=> [
|
||||
$this->loadEvents(),
|
||||
$this->loadBoardEvents(),
|
||||
$this->loadVotes(),
|
||||
$this->loadBoardVotes(),
|
||||
]);
|
||||
|
||||
on([
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function($pubkey) {
|
||||
$this->currentPubkey = $pubkey;
|
||||
$allowedPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if(in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
dd($this->isAllowed);
|
||||
},
|
||||
'echo:votes,.newVote' => fn()
|
||||
=> [
|
||||
$this->loadEvents(),
|
||||
$this->loadBoardEvents(),
|
||||
$this->loadVotes(),
|
||||
$this->loadBoardVotes(),
|
||||
],
|
||||
]);
|
||||
|
||||
$loadVotes = function () {
|
||||
$this->votes = collect($this->events)
|
||||
->map(fn($event)
|
||||
=> [
|
||||
'created_at' => $event['created_at'],
|
||||
'pubkey' => $event['pubkey'],
|
||||
'forpubkey' => $this->fetchProfile($event['content']),
|
||||
'type' => str($event['content'])->after(',')->toString(),
|
||||
])
|
||||
->sortByDesc('created_at')
|
||||
->unique(fn($event) => $event['pubkey'] . $event['type'])
|
||||
->values()
|
||||
->groupBy('type')
|
||||
->map(fn($votes)
|
||||
=> [
|
||||
'type' => $votes[0]['type'],
|
||||
'votes' => $votes->groupBy('forpubkey')->map(fn($group) => ['count' => $group->count()])->toArray(),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$loadBoardVotes = function () {
|
||||
$this->boardVotes = collect($this->boardEvents)
|
||||
->map(fn($event)
|
||||
=> [
|
||||
'created_at' => $event['created_at'],
|
||||
'pubkey' => $event['pubkey'],
|
||||
'forpubkey' => $this->fetchProfile($event['content']),
|
||||
'type' => str($event['content'])->after(',')->toString(),
|
||||
])
|
||||
->sortByDesc('created_at')
|
||||
->values()
|
||||
->groupBy('type')
|
||||
->map(fn($votes)
|
||||
=> [
|
||||
'type' => $votes[0]['type'],
|
||||
'votes' => $votes->groupBy('forpubkey')->map(fn($group) => ['count' => $group->count()])->toArray(),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$loadEvents = function () {
|
||||
$this->events = $this->loadNostrEvents([32122]);
|
||||
};
|
||||
|
||||
$loadBoardEvents = function () {
|
||||
$this->boardEvents = $this->loadNostrEvents([2121]);
|
||||
};
|
||||
|
||||
$fetchProfile = function ($content) {
|
||||
$pubkey = str($content)->before(',')->toString();
|
||||
$profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first();
|
||||
if (!$profile) {
|
||||
Artisan::call(\App\Console\Commands\Nostr\FetchProfile::class, ['--pubkey' => $pubkey]);
|
||||
$profile = \App\Models\Profile::query()->where('pubkey', $pubkey)->first();
|
||||
}
|
||||
return $profile->pubkey;
|
||||
};
|
||||
|
||||
$loadNostrEvents = function ($kinds) {
|
||||
$subscription = new Subscription();
|
||||
$subscriptionId = $subscription->setId();
|
||||
$filter = new Filter();
|
||||
$filter->setKinds($kinds);
|
||||
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
|
||||
$relaySet = new RelaySet();
|
||||
$relaySet->setRelays([new Relay(config('services.relay'))]);
|
||||
$request = new Request($relaySet, $requestMessage);
|
||||
$response = $request->send();
|
||||
return collect($response[config('services.relay')])
|
||||
->map(function($event) {
|
||||
if(!isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->toArray();
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="{{ __('Wahl Manager') }}">
|
||||
@volt
|
||||
@php
|
||||
$positions = [
|
||||
'presidency' => ['icon' => 'fa-crown', 'title' => 'Präsidium'],
|
||||
'board' => ['icon' => 'fa-users', 'title' => 'Vorstandsmitglieder'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@if($isAllowed)
|
||||
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="electionAdminCharts(@this)">
|
||||
|
||||
<!-- Dashboard actions -->
|
||||
<div class="sm:flex sm:justify-between sm:items-center mb-8">
|
||||
|
||||
<!-- 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">
|
||||
Wahl des Vorstands {{ $election->year }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@php
|
||||
$president = $positions['presidency'];
|
||||
$board = $positions['board'];
|
||||
@endphp
|
||||
|
||||
<!-- Cards -->
|
||||
<div class="grid gap-y-4">
|
||||
<div wire:key="presidency" wire:ignore
|
||||
class="flex flex-col bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
||||
class="fa-sharp-duotone fa-solid {{ $president['icon'] }} w-5 h-5 fill-current text-white mr-4"></i>{{ $president['title'] }}
|
||||
</h2>
|
||||
</header>
|
||||
<div class="grow">
|
||||
<!-- Change the height attribute to adjust the chart height -->
|
||||
<canvas x-ref="chart_presidency" width="724" height="288"
|
||||
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div wire:key="board" wire:ignore
|
||||
class="flex flex-col bg-white dark:bg-gray-800 shadow-sm rounded-xl">
|
||||
<header class="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100"><i
|
||||
class="fa-sharp-duotone fa-solid {{ $board['icon'] }} w-5 h-5 fill-current text-white mr-4"></i>{{ $board['title'] }}
|
||||
</h2>
|
||||
</header>
|
||||
<div class="grow">
|
||||
<!-- Change the height attribute to adjust the chart height -->
|
||||
<canvas x-ref="chart_board" width="724" height="288"
|
||||
style="display: block; box-sizing: border-box; height: 288px; width: 724px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@else
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">Mitglieder</h3>
|
||||
<p class="mt-1 max-w">
|
||||
Du bist nicht berechtigt, Mitglieder zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,103 +0,0 @@
|
||||
<?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(['isAllowed' => false]);
|
||||
state(['currentPubkey' => null]);
|
||||
state(['elections' => []]);
|
||||
|
||||
mount(function () {
|
||||
$this->elections = \App\Models\Election::query()
|
||||
->get()
|
||||
->toArray();
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$logPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->where('pubkey', $pubkey)->first();
|
||||
$logPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $logPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
$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>
|
||||
@if($isAllowed)
|
||||
<div class="relative flex h-full">
|
||||
@foreach($elections as $election)
|
||||
<div class="w-full sm: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>
|
||||
@else
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">Einstellungen</h3>
|
||||
<p class="mt-1 max-w">
|
||||
Du bist nicht berechtigt, die Einstellungen zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
use function Livewire\Volt\{
|
||||
computed,
|
||||
mount,
|
||||
state,
|
||||
with,
|
||||
updated,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
name('association.members.admin');
|
||||
|
||||
state(['isAllowed' => false]);
|
||||
state(['currentPubkey' => null]);
|
||||
state(['members' => []]);
|
||||
|
||||
mount(function () {
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->where('pubkey', $this->currentPubkey )->first();
|
||||
$allowedPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
'7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d',
|
||||
'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
|
||||
'19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3',
|
||||
'acbcec475a1a4f9481939ecfbd1c3d111f5b5a474a39ae039bbc720fdd305bec',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
},
|
||||
]);
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()
|
||||
->where('pubkey', $pubkey)->first();
|
||||
$allowedPubkeys = [
|
||||
'0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
|
||||
'430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
|
||||
'7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d',
|
||||
'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
|
||||
'19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3',
|
||||
'acbcec475a1a4f9481939ecfbd1c3d111f5b5a474a39ae039bbc720fdd305bec',
|
||||
];
|
||||
if (in_array($this->currentPubkey, $allowedPubkeys, true)) {
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="{{ __('Mitglieder') }}">
|
||||
@volt
|
||||
<div>
|
||||
@if($isAllowed)
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<livewire:einundzwanzig-pleb-table/>
|
||||
</div>
|
||||
@else
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
||||
<div class="bg-white dark:bg-[#1B1B1B] shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">Mitglieder</h3>
|
||||
<p class="mt-1 max-w">
|
||||
Du bist nicht berechtigt, Mitglieder zu bearbeiten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,187 +0,0 @@
|
||||
<?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 WireUi\Actions\Notification;
|
||||
|
||||
use function Laravel\Folio\{middleware, name};
|
||||
use function Livewire\Volt\{state, mount, on, computed, updated};
|
||||
|
||||
name('association.projectSupport');
|
||||
|
||||
state(['activeFilter' => 'all',])->url();
|
||||
|
||||
state([
|
||||
'search' => '',
|
||||
'projects' => fn()
|
||||
=> \App\Models\ProjectProposal::query()
|
||||
->with([
|
||||
'einundzwanzigPleb.profile',
|
||||
'votes',
|
||||
])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get(),
|
||||
'isAllowed' => false,
|
||||
'currentPubkey' => null,
|
||||
'currentPleb' => null,
|
||||
]);
|
||||
|
||||
updated([
|
||||
'search' => function () {
|
||||
$this->projects = \App\Models\ProjectProposal::query()
|
||||
->with([
|
||||
'einundzwanzigPleb.profile',
|
||||
'votes',
|
||||
])
|
||||
->where(function ($query) {
|
||||
$query
|
||||
->where('name', 'ilike', '%'.$this->search.'%')
|
||||
->orWhere('description', 'ilike', '%'.$this->search.'%')
|
||||
->orWhereHas('einundzwanzigPleb.profile', function ($q) {
|
||||
$q->where('name', 'ilike', '%'.$this->search.'%');
|
||||
});
|
||||
})
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
},
|
||||
]);
|
||||
|
||||
$projects = computed(function () {
|
||||
return $this->projects;
|
||||
});
|
||||
|
||||
mount(function () {
|
||||
if (\App\Support\NostrAuth::check()) {
|
||||
$this->currentPubkey = \App\Support\NostrAuth::pubkey();
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $this->currentPubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
}
|
||||
});
|
||||
|
||||
on([
|
||||
'nostrLoggedIn' => function ($pubkey) {
|
||||
\App\Support\NostrAuth::login($pubkey);
|
||||
$this->currentPubkey = $pubkey;
|
||||
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
|
||||
$this->isAllowed = true;
|
||||
},
|
||||
'nostrLoggedOut' => function () {
|
||||
$this->isAllowed = false;
|
||||
$this->currentPubkey = null;
|
||||
$this->currentPleb = null;
|
||||
},
|
||||
]);
|
||||
|
||||
$confirmDelete = function ($id) {
|
||||
$notification = new Notification($this);
|
||||
$notification->confirm([
|
||||
'title' => 'Projektunterstützung löschen',
|
||||
'message' => 'Bist du sicher, dass du diese Projektunterstützung löschen möchtest?',
|
||||
'accept' => [
|
||||
'label' => 'Ja, löschen',
|
||||
'method' => 'delete',
|
||||
'params' => $id,
|
||||
],
|
||||
]);
|
||||
};
|
||||
|
||||
$setFilter = function ($filter) {
|
||||
$this->activeFilter = $filter;
|
||||
};
|
||||
|
||||
$delete = function ($id) {
|
||||
\App\Models\ProjectProposal::query()->findOrFail($id)->delete();
|
||||
$this->projects = \App\Models\ProjectProposal::query()
|
||||
->with([
|
||||
'einundzwanzigPleb.profile',
|
||||
'votes',
|
||||
])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app
|
||||
:seo="new \RalphJSmit\Laravel\SEO\Support\SEOData(title: 'Projekt Unterstützungen', description: 'Einundzwanzig Projektunterstützungen')"
|
||||
>
|
||||
@volt
|
||||
<div>
|
||||
<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-cols-1 sm:grid-cols-2 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 -->
|
||||
@if($currentPleb && $currentPleb->association_status->value > 1 && $currentPleb->paymentEvents()->where('year', date('Y'))->where('paid', true)->exists())
|
||||
<x-button :href="route('association.projectSupport.create')" icon="plus"
|
||||
label="Projekt einreichen"/>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-5">
|
||||
<ul class="flex flex-wrap -m-1">
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('all')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border {{ $activeFilter === 'all' ? 'border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800' : '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">
|
||||
Alle
|
||||
</button>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('new')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border {{ $activeFilter === 'new' ? 'border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800' : '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">
|
||||
Neu
|
||||
</button>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('supported')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border {{ $activeFilter === 'supported' ? 'border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800' : '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">
|
||||
Unterstützt
|
||||
</button>
|
||||
</li>
|
||||
<li class="m-1">
|
||||
<button wire:click="setFilter('rejected')"
|
||||
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border {{ $activeFilter === 'rejected' ? 'border-transparent shadow-sm bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-800' : '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">
|
||||
Abgelehnt
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 italic mb-4">{{ $this->projects->count() }}Projekte
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="grid xl:grid-cols-2 gap-6 mb-8">
|
||||
@foreach($this->projects as $project)
|
||||
<x-project-card :project="$project" :currentPleb="$currentPleb" :section="$activeFilter"/>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,24 +0,0 @@
|
||||
<?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, name};
|
||||
use function Livewire\Volt\{state, mount, on, computed};
|
||||
|
||||
name('welcome');
|
||||
|
||||
?>
|
||||
|
||||
<x-layouts.app title="Welcome">
|
||||
@volt
|
||||
<div>
|
||||
TEST
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,145 +0,0 @@
|
||||
<?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,
|
||||
mount,
|
||||
state,
|
||||
with,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
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(function($event) {
|
||||
if(!isset($event->event)) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->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>
|
||||
@@ -1,34 +0,0 @@
|
||||
<?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,
|
||||
mount,
|
||||
state,
|
||||
with,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
name('meetups.table');
|
||||
|
||||
?>
|
||||
|
||||
|
||||
<x-layouts.app title="{{ __('Meetups') }}">
|
||||
@volt
|
||||
<div>
|
||||
<livewire:meetup-table />
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,76 +0,0 @@
|
||||
<?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,
|
||||
mount,
|
||||
state,
|
||||
with,
|
||||
on
|
||||
};
|
||||
use function Laravel\Folio\{
|
||||
middleware,
|
||||
name
|
||||
};
|
||||
|
||||
name('meetups.worldmap');
|
||||
|
||||
with(['markers' => []])
|
||||
|
||||
?>
|
||||
|
||||
@push('scripts')
|
||||
<script src="{{ asset('dist/jquery.js') }}"></script>
|
||||
<script src="{{ asset('vendor/jvector/jquery-jvectormap-2.0.5.min.js') }}"></script>
|
||||
<script src="{{ asset('vendor/jvector/maps/world-mill.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ asset('vendor/jvector/jquery-jvectormap-2.0.5.css') }}" type="text/css"
|
||||
media="screen"/>
|
||||
@endpush
|
||||
|
||||
<x-layouts.app title="{{ __('Worldmap') }}">
|
||||
@volt
|
||||
<div
|
||||
wire:ignore
|
||||
class="w-full flex justify-center"
|
||||
x-data="{
|
||||
init() {
|
||||
let markers = {{ Js::from($markers) }};
|
||||
|
||||
$('#mapworld').vectorMap({
|
||||
zoomButtons : true,
|
||||
zoomOnScroll: true,
|
||||
map: 'world_mill',
|
||||
backgroundColor: 'transparent',
|
||||
markers: markers.map(function(h){ return {name: h.name, latLng: h.coords} }),
|
||||
onMarkerClick: function(event, index) {
|
||||
$wire.call('filterByMarker', markers[index].id)
|
||||
},
|
||||
markerStyle: {
|
||||
initial: {
|
||||
image: '{{ asset('img/btc.png') }}',
|
||||
}
|
||||
},
|
||||
regionStyle: {
|
||||
initial: {
|
||||
fill: '#a4a4a4'
|
||||
},
|
||||
hover: {
|
||||
'fill-opacity': 1,
|
||||
cursor: 'default'
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}"
|
||||
>
|
||||
<div id="mapworld" style="width: 100%;" class="h-[200px] sm:h-[400px]"></div>
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
@@ -1,5 +1,22 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Association\Election\Admin as ElectionAdmin;
|
||||
use App\Livewire\Association\Election\Index as ElectionIndex;
|
||||
use App\Livewire\Association\Election\Show as ElectionShow;
|
||||
use App\Livewire\Association\Members\Admin as MembersAdmin;
|
||||
use App\Livewire\Association\News\Index as NewsIndex;
|
||||
use App\Livewire\Association\Profile;
|
||||
use App\Livewire\Association\ProjectSupport\Form\Create as ProjectSupportCreate;
|
||||
use App\Livewire\Association\ProjectSupport\Form\Edit as ProjectSupportEdit;
|
||||
use App\Livewire\Association\ProjectSupport\Index as ProjectSupportIndex;
|
||||
use App\Livewire\Association\ProjectSupport\Show as ProjectSupportShow;
|
||||
use App\Livewire\Changelog;
|
||||
use App\Livewire\EinundzwanzigFeed\Index as EinundzwanzigFeedIndex;
|
||||
use App\Livewire\Meetups\Grid as MeetupsGrid;
|
||||
use App\Livewire\Meetups\Mockup as MeetupsMockup;
|
||||
use App\Livewire\Meetups\Table as MeetupsTable;
|
||||
use App\Livewire\Meetups\Worldmap as MeetupsWorldmap;
|
||||
use App\Livewire\Welcome;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
@@ -16,5 +33,35 @@ Route::get('dl/{media}', function (Media $media, Request $request) {
|
||||
Route::post('logout', function () {
|
||||
\App\Support\NostrAuth::logout();
|
||||
Session::flush();
|
||||
|
||||
return redirect('/');
|
||||
})->name('logout');
|
||||
|
||||
// Association Routes
|
||||
Route::livewire('/association/profile', Profile::name('association.profile'));
|
||||
|
||||
Route::livewire('/association/election', ElectionIndex::class)->name('association.elections');
|
||||
Route::livewire('/association/election/{election:year}', ElectionShow::class)->name('association.election');
|
||||
Route::livewire('/association/election/admin/{election:year}', ElectionAdmin::class)->name('association.election.admin');
|
||||
|
||||
Route::livewire('/association/members/admin', MembersAdmin::class)->name('association.members.admin');
|
||||
|
||||
Route::livewire('/association/news', NewsIndex::class)->name('association.news');
|
||||
|
||||
Route::livewire('/association/project-support', ProjectSupportIndex::class)->name('association.projectSupport');
|
||||
Route::livewire('/association/project-support/create', ProjectSupportCreate::class)->name('association.projectSupport.create');
|
||||
Route::livewire('/association/project-support/{projectProposal:slug}', ProjectSupportShow::class)->name('association.projectSupport.item');
|
||||
Route::livewire('/association/project-support/edit/{projectProposal:slug}', ProjectSupportEdit::class)->name('association.projectSupport.edit');
|
||||
|
||||
// Einundzwanzig Feed
|
||||
Route::livewire('/einundzwanzig-feed', EinundzwanzigFeedIndex::class)->name('einundzwanzig-feed');
|
||||
|
||||
// Meetups
|
||||
Route::livewire('/meetups/grid', MeetupsGrid::class)->name('meetups.grid');
|
||||
Route::livewire('/meetups/mockup', MeetupsMockup::class)->name('meetups.mockup');
|
||||
Route::livewire('/meetups/table', MeetupsTable::class)->name('meetups.table');
|
||||
Route::livewire('/meetups/worldmap', MeetupsWorldmap::class)->name('meetups.worldmap');
|
||||
|
||||
// Other pages
|
||||
Route::livewire('/changelog', Changelog::class)->name('changelog');
|
||||
Route::livewire('/welcome', Welcome::class)->name('welcome');
|
||||
|
||||
2
storage/pail/.gitignore
vendored
Normal file
2
storage/pail/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
Reference in New Issue
Block a user