Compare commits

..

No commits in common. "44ea68cce7fda202d810d7d3e5b15a3713ac9db5" and "d692cf3bdf6c85c99f55a79e9a85208d0a366ca1" have entirely different histories.

207 changed files with 1698 additions and 10586 deletions

View File

@ -5,7 +5,7 @@ RUN composer install --ignore-platform-reqs --no-dev
RUN php artisan telescope:publish
RUN php artisan horizon:publish
FROM node:18.13.0-slim as node
FROM node:17.9.0-slim as node
WORKDIR /app
COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules

View File

@ -22,9 +22,9 @@ steps:
- while ! mysqladmin ping -h db -u db -pdb --silent; do sleep 1; done
- name: node
image: node:18.13.0-slim
image: node:17.9.0-slim
commands:
- npm ci && cd packages/adrema-plugin && npm ci && npm run build && cd ../../ && npm run img && npm run prod && rm -R node_modules
- npm ci && npm run img && npm run prod && rm -R node_modules
- name: tests
image: zoomyboy/adrema-base:latest

7
.gitmodules vendored
View File

@ -7,10 +7,3 @@
[submodule "packages/tex"]
path = packages/tex
url = https://git.zoomyboy.de/pille/tex
[submodule "packages/adrema-form"]
path = packages/adrema-form
url = https://git.zoomyboy.de/silva/adrema-form.git
[submodule "packages/medialibrary-helper"]
path = packages/medialibrary-helper
url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git
branch = version2

View File

@ -1,33 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form;
use App\Form\Resources\FormApiResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormApiListAction
{
use AsAction;
/**
* @param string $filter
* @return LengthAwarePaginator<Form>
*/
public function handle(string $filter, int $perPage): LengthAwarePaginator
{
return FormFilterScope::fromRequest($filter)->getQuery()->paginate($perPage);
}
public function asController(ActionRequest $request): AnonymousResourceCollection
{
return FormApiResource::collection($this->handle(
$request->input('filter', ''),
$request->input('perPage', 10)
));
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Lorisleiva\Actions\Concerns\AsAction;
class FormDestroyAction
{
use AsAction;
public function asController(Form $form): void
{
$form->delete();
Succeeded::message('Veranstaltung gelöscht.')->dispatch();
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form;
use App\Form\Resources\FormResource;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormIndexAction
{
use AsAction;
/**
* @return LengthAwarePaginator<Form>
*/
public function handle(string $filter): LengthAwarePaginator
{
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15);
}
public function asController(ActionRequest $request): Response
{
session()->put('menu', 'form');
session()->put('title', 'Veranstaltungen');
return Inertia::render('form/Index', [
'data' => FormResource::collection($this->handle($request->input('filter', ''))),
]);
}
}

View File

@ -1,69 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\ActionRequest;
class FormStoreAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
'description.time' => 'required|integer',
'description.blocks' => 'required|array',
'description.version' => 'required|string',
'excerpt' => 'required|string|max:130',
'from' => 'required|date',
'to' => 'required|date',
'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date',
'mail_top' => 'nullable|string',
'mail_bottom' => 'nullable|string',
'header_image' => 'required|exclude',
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(array $attributes): Form
{
return tap(
Form::create($attributes),
fn ($form) => $form->setDeferredUploads(request()->input('header_image'))
);
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
'from' => 'Start',
'to' => 'Ende',
'header_image' => 'Bild',
'description.blocks' => 'Beschreibung',
];
}
public function asController(ActionRequest $request): JsonResponse
{
$this->handle($request->validated());
Succeeded::message('Veranstaltung gespeichert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,66 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\ActionRequest;
class FormUpdateAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
'description' => 'required|array',
'description.time' => 'required|integer',
'description.blocks' => 'required|array',
'description.version' => 'required|string',
'excerpt' => 'required|string|max:130',
'from' => 'required|date',
'to' => 'required|date',
'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date',
'mail_top' => 'nullable|string',
'mail_bottom' => 'nullable|string',
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(Form $form, array $attributes): Form
{
$form->update($attributes);
return $form;
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
'from' => 'Start',
'to' => 'Ende',
'description.blocks' => 'Beschreibung',
];
}
public function asController(Form $form, ActionRequest $request): JsonResponse
{
$this->handle($form, $request->validated());
Succeeded::message('Veranstaltung aktualisiert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,46 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use Illuminate\Validation\Rule;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\HttpFoundation\JsonResponse;
class FormUpdateMetaAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Form */
$form = request()->route('form');
return [
'sorting' => 'array',
'sorting.0' => 'required|string',
'sorting.1' => 'required|string|in:asc,desc',
'active_columns' => 'array',
'active_columns.*' => ['string', Rule::in($form->getFields()->pluck('key')->toArray())]
];
}
/**
* @param array<string, mixed> $input
*/
public function handle(Form $form, array $input): void
{
$form->update(['meta' => $input]);
}
public function asController(Form $form, ActionRequest $request): JsonResponse
{
$this->handle($form, $request->validated());
return response()->json($form->fresh()->meta);
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateDestroyAction
{
use AsAction;
public function handle(Formtemplate $formtemplate): void
{
$formtemplate->delete();
}
public function asController(Formtemplate $formtemplate): JsonResponse
{
$this->handle($formtemplate);
Succeeded::message('Vorlage gelöscht.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Form\Resources\FormtemplateResource;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateIndexAction
{
use AsAction;
/**
* @return LengthAwarePaginator<Formtemplate>
*/
public function handle(): LengthAwarePaginator
{
return Formtemplate::paginate(15);
}
public function asController(): Response
{
session()->put('menu', 'form');
session()->put('title', 'Formular-Vorlagen');
return Inertia::render('formtemplate/Index', [
'data' => FormtemplateResource::collection($this->handle()),
]);
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateStoreAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
];
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(array $attributes): Formtemplate
{
return Formtemplate::create($attributes);
}
public function asController(ActionRequest $request): JsonResponse
{
$this->handle($request->validated());
Succeeded::message('Vorlage gespeichert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateUpdateAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
];
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(Formtemplate $formtemplate, array $attributes): void
{
$formtemplate->update($attributes);
}
public function asController(Formtemplate $formtemplate, ActionRequest $request): JsonResponse
{
$this->handle($formtemplate, $request->validated());
Succeeded::message('Vorlage aktualisiert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace App\Form\Actions;
use Illuminate\Validation\Rule;
use App\Form\Fields\Field;
use Illuminate\Validation\Validator;
use Lorisleiva\Actions\ActionRequest;
trait HasValidation
{
/**
* @return array<string, mixed>
*/
public function globalRules(): array
{
return [
'name' => 'required|string|max:255',
'config' => 'array',
'config.sections.*.name' => 'required',
'config.sections.*.fields' => 'array',
'config.sections.*.fields.*.name' => 'required|string',
'config.sections.*.fields.*.type' => ['required', 'string', Rule::in(array_column(Field::asMeta(), 'id'))],
'config.sections.*.fields.*.key' => ['required', 'string', 'regex:/^[a-zA-Z_]*$/'],
'config.sections.*.fields.*.columns' => 'required|array',
'config.sections.*.fields.*.*' => '',
'config.sections.*.fields.*.columns.mobile' => 'required|numeric|gt:0|lte:2',
'config.sections.*.fields.*.columns.tablet' => 'required|numeric|gt:0|lte:4',
'config.sections.*.fields.*.columns.desktop' => 'required|numeric|gt:0|lte:6',
];
}
/**
* @return array<string, mixed>
*/
public function globalValidationAttributes(): array
{
return [
'config.sections.*.name' => 'Sektionsname',
'config.sections.*.fields.*.name' => 'Feldname',
'config.sections.*.fields.*.type' => 'Feldtyp',
'config.sections.*.fields.*.key' => 'Feldkey',
];
}
public function withValidator(Validator $validator, ActionRequest $request): void
{
if (!$validator->passes()) {
return;
}
foreach ($request->input('config.sections') as $sindex => $section) {
foreach (data_get($section, 'fields') as $findex => $field) {
$fieldClass = Field::classFromType($field['type']);
if (!$fieldClass) {
continue;
}
foreach ($fieldClass::metaRules() as $fieldName => $rules) {
$validator->addRules(["config.sections.{$sindex}.fields.{$findex}.{$fieldName}" => $rules]);
}
foreach ($fieldClass::metaAttributes() as $fieldName => $attribute) {
$validator->addCustomAttributes(["config.sections.{$sindex}.fields.{$findex}.{$fieldName}" => $attribute]);
}
}
}
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Resources\ParticipantResource;
use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantIndexAction
{
use AsAction;
/**
* @return LengthAwarePaginator<Participant>
*/
public function handle(Form $form, ParticipantFilterScope $filter): LengthAwarePaginator
{
return $form->participants()->withFilter($filter)->with('form')->paginate(15);
}
public function asController(Form $form): AnonymousResourceCollection
{
$filter = ParticipantFilterScope::fromRequest(request()->input('filter'));
return ParticipantResource::collection($this->handle($form, $filter))
->additional(['meta' => ParticipantResource::meta($form)]);
}
}

View File

@ -1,78 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Data\FieldCollection;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Member\Member;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class RegisterAction
{
use AsAction;
/**
* @param array<string, mixed> $input
*/
public function handle(Form $form, array $input): Participant
{
$memberQuery = FieldCollection::fromRequest($form, $input)
->withNamiType()
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
$mglnr = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first()->mitgliedsnr : null;
$participant = $form->participants()->create([
'data' => $input,
'mitgliedsnr' => $mglnr,
]);
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
$participant->sendConfirmationMail();
return $participant;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Form */
$form = request()->route('form');
return $form->getRegistrationRules();
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
/** @var Form */
$form = request()->route('form');
return $form->getRegistrationAttributes();
}
/**
* @return array<string, mixed>
*/
public function getValidationMessages(): array
{
/** @var Form */
$form = request()->route('form');
return $form->getRegistrationMessages();
}
public function asController(ActionRequest $request, Form $form): JsonResponse
{
$participant = $this->handle($form, $request->validated());
return response()->json($participant);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Form\Casts;
use App\Form\Fields\Field;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Support\DataProperty;
class CollectionCast implements Cast
{
/**
* @param class-string<Data> $target
*/
public function __construct(public string $target)
{
}
/**
* @param array<int, array<string, mixed>> $value
* @param array<string, mixed> $context
* @return Collection<int, Data>
*/
public function cast(DataProperty $property, mixed $value, array $context): mixed
{
return collect($value)->map(fn ($item) => $this->target::from($item));
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Form\Casts;
use App\Form\Data\FieldCollection;
use App\Form\Fields\Field;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Support\DataProperty;
class FieldCollectionCast implements Cast
{
/**
* @param array<int, array<string, mixed>> $value
* @param array<string, mixed> $context
* @return FieldCollection
*/
public function cast(DataProperty $property, mixed $value, array $context): mixed
{
return new FieldCollection(collect($value)->map(fn ($value) => Field::classFromType($value['type'])::from($value))->all());
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Form\Data;
use Spatie\LaravelData\Data;
class ColumnData extends Data
{
public function __construct(
public int $mobile,
public int $tablet,
public int $desktop,
) {
}
}

View File

@ -1,91 +0,0 @@
<?php
namespace App\Form\Data;
use App\Form\Enums\SpecialType;
use App\Form\Fields\Field;
use App\Form\Fields\NamiField;
use App\Form\Models\Form;
use Illuminate\Support\Collection;
use stdClass;
/**
* @extends Collection<int, Field>
*/
class FieldCollection extends Collection
{
public function forMembers(): self
{
return $this->filter(fn ($field) => $field->forMembers === true);
}
public function withNamiType(): self
{
return $this->filter(fn ($field) => $field->namiType !== null);
}
public function noNamiType(): self
{
return $this->filter(fn ($field) => $field->namiType === null);
}
public function noNamiField(): self
{
return $this->filter(fn ($field) => !is_a($field, NamiField::class));
}
/**
* @return stdClass
*/
public function getMailRecipient(): ?stdClass
{
$email = $this->findBySpecialType(SpecialType::EMAIL)?->value;
return $this->getFullname() && $email
? (object) [
'name' => $this->getFullname(),
"email" => $email,
] : null;
}
public function getFullname(): ?string
{
$firstname = $this->findBySpecialType(SpecialType::FIRSTNAME)?->value;
$lastname = $this->findBySpecialType(SpecialType::LASTNAME)?->value;
return $firstname && $lastname ? "$firstname $lastname" : null;
}
/**
* @param array<string, mixed> $input
*/
public static function fromRequest(Form $form, array $input): self
{
return $form->getFields()->each(fn ($field) => $field->value = array_key_exists($field->key, $input) ? $input[$field->key] : $field->default());
}
public function find(Field $givenField): ?Field
{
return $this->first(fn ($field) => $field->key === $givenField->key);
}
/**
* @return array<string, mixed>
*/
public function present(): array
{
$attributes = collect([]);
foreach ($this as $field) {
$attributes = $attributes->merge($field->present());
}
return $attributes->toArray();
}
private function findBySpecialType(SpecialType $specialType): ?Field
{
return $this->first(fn ($field) => $field->specialType === $specialType);
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Form\Data;
use App\Form\Casts\CollectionCast;
use App\Form\Transformers\CollectionTransformer;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
class FormConfigData extends Data
{
/**
* @param Collection<int, SectionData> $sections
*/
public function __construct(
#[WithCast(CollectionCast::class, target: SectionData::class)]
#[WithTransformer(CollectionTransformer::class, target: SectionData::class)]
public Collection $sections
) {
}
public function fields(): FieldCollection
{
return $this->sections->reduce(
fn ($carry, $current) => $carry->merge($current->fields->all()),
new FieldCollection([])
);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Form\Data;
use App\Form\Casts\FieldCollectionCast;
use Spatie\LaravelData\Data;
use App\Form\Transformers\FieldCollectionTransformer;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
class SectionData extends Data
{
public function __construct(
public string $name,
#[WithCast(FieldCollectionCast::class)]
#[WithTransformer(FieldCollectionTransformer::class)]
public FieldCollection $fields
) {
}
}

View File

@ -1,77 +0,0 @@
<?php
namespace App\Form\Enums;
use App\Group\Enums\Level;
use App\Member\Member;
use Illuminate\Database\Eloquent\Builder;
enum NamiType: string
{
case FIRSTNAME = 'Vorname';
case LASTNAME = 'Nachname';
case BIRTHDAY = 'Geburtstag';
case REGION = 'Bezirk';
case STAMM = 'Stamm';
case EMAIL = 'E-Mail-Adresse';
/**
* @return array<int, array{name: string, id: string}>
*/
public static function forSelect(): array
{
return collect(static::cases())
->map(fn ($case) => ['id' => $case->value, 'name' => $case->value])
->toArray();
}
public function getMemberAttribute(Member $member): ?string
{
return match ($this) {
static::FIRSTNAME => $member->firstname,
static::LASTNAME => $member->lastname,
static::BIRTHDAY => $member->birthday?->format('Y-m-d'),
static::REGION => $this->matchRegion($member),
static::STAMM => $this->matchGroup($member),
static::EMAIL => $member->email,
};
}
/**
* @param Builder<Member> $query
* @return Builder<Member>
*/
public function performQuery(Builder $query, mixed $value): Builder
{
return match ($this) {
static::FIRSTNAME => $query->where('firstname', $value),
static::LASTNAME => $query->where('lastname', $value),
static::BIRTHDAY => $query->where('birthday', $value),
static::REGION => $query,
static::STAMM => $query,
static::EMAIL => $query->where('email', $value),
};
}
protected function matchGroup(Member $member): ?int
{
if ($member->group->level === Level::GROUP) {
return $member->group_id;
}
return null;
}
protected function matchRegion(Member $member): ?int
{
if ($member->group->parent?->level === Level::REGION) {
return $member->group->parent->id;
}
if ($member->group->level === Level::REGION) {
return $member->group_id;
}
return null;
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Form\Enums;
enum SpecialType: string
{
case FIRSTNAME = 'Vorname';
case LASTNAME = 'Nachname';
case EMAIL = 'E-Mail-Adresse';
/**
* @return array<int, array{name: string, id: string}>
*/
public static function forSelect(): array
{
return collect(static::cases())
->map(fn ($case) => ['id' => $case->value, 'name' => $case->value])
->toArray();
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Faker\Generator;
use Illuminate\Validation\Rule;
class CheckboxField extends Field
{
public bool $required;
public string $description;
public static function name(): string
{
return 'Checkbox';
}
public static function meta(): array
{
return [
['key' => 'description', 'default' => '', 'rules' => ['description' => 'required|string'], 'label' => 'Beschreibung'],
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
];
}
public static function default()
{
return false;
}
public static function fake(Generator $faker): array
{
return [
'description' => $faker->text(),
'required' => $faker->boolean(),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
return [
$this->key => $this->required ? ['boolean', 'accepted'] : ['present', 'boolean'],
];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [
$this->key => $this->name,
];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\EnumPresenter;
use App\Form\Presenters\Presenter;
use Faker\Generator;
use Illuminate\Validation\Rule;
class CheckboxesField extends Field
{
/** @var array<int, string> */
public array $options;
public static function name(): string
{
return 'Checkboxes';
}
public static function meta(): array
{
return [
['key' => 'options', 'default' => [], 'rules' => ['options' => 'array', 'options.*' => 'required|string'], 'label' => 'Optionen'],
];
}
public static function default()
{
return [];
}
public static function fake(Generator $faker): array
{
return [
'options' => $faker->words(4),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
return [
$this->key => 'array',
$this->key . '.*' => ['string', Rule::in($this->options)],
];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [
...collect($this->options)->mapWithKeys(fn ($option, $key) => [$this->key . '.' . $key => $this->name])->toArray(),
$this->key => $this->name,
];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
public function getPresenter(): Presenter
{
return app(EnumPresenter::class);
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,92 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Contracts\Displayable;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\DatePresenter;
use App\Form\Presenters\Presenter;
use Carbon\Carbon;
use Faker\Generator;
class DateField extends Field
{
public bool $required;
public bool $maxToday;
public static function name(): string
{
return 'Datum';
}
public static function meta(): array
{
return [
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
['key' => 'max_today', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Nur daten bis heute erlauben'],
];
}
public static function default(): ?string
{
return null;
}
public static function fake(Generator $faker): array
{
return [
'required' => $faker->boolean(),
'max_today' => $faker->boolean(),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
$rules = [$this->required ? 'required' : 'nullable'];
$rules[] = 'date';
if ($this->maxToday) {
$rules[] = 'before_or_equal:' . now()->format('Y-m-d');
}
return [$this->key => $rules];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [
$this->key => $this->name,
];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [
$this->key . '.before_or_equal' => $this->name . ' muss ein Datum vor oder gleich dem ' . now()->format('d.m.Y') . ' sein.',
];
}
public function getPresenter(): Presenter
{
return app(DatePresenter::class);
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Faker\Generator;
use Illuminate\Validation\Rule;
class DropdownField extends Field
{
public bool $required;
/** @var array<int, string> */
public array $options;
public static function name(): string
{
return 'Dropdown';
}
public static function meta(): array
{
return [
['key' => 'options', 'default' => [], 'rules' => ['options' => 'present|array', 'options.*' => 'string'], 'label' => 'Optionen'],
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
];
}
public static function default()
{
return null;
}
public static function fake(Generator $faker): array
{
return [
'options' => $faker->words(4),
'required' => $faker->boolean(),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
return [
$this->key => $this->required ? ['required', 'string', Rule::in($this->options)] : ['nullable', 'string', Rule::in($this->options)],
];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [$this->key => $this->name];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,167 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Data\ColumnData;
use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\DefaultPresenter;
use App\Form\Presenters\Presenter;
use Faker\Generator;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
abstract class Field extends Data
{
public string $key;
public string $name;
public ?NamiType $namiType = null;
public ColumnData $columns;
public bool $forMembers;
public ?SpecialType $specialType = null;
/** @var mixed */
public $value;
/**
* @param array<array-key, mixed> $input
*/
abstract public function afterRegistration(Form $form, Participant $participant, array $input): void;
abstract public static function name(): string;
/** @return array<int, array{key: string, default: mixed, label: string, rules: array<string, mixed>}> */
abstract public static function meta(): array;
/** @return mixed */
abstract public static function default();
/** @return array<string, mixed> */
abstract public function getRegistrationRules(Form $form): array;
/** @return array<string, mixed> */
abstract public function getRegistrationAttributes(Form $form): array;
/** @return array<string, mixed> */
abstract public function getRegistrationMessages(Form $form): array;
/** @return array<string, mixed> */
abstract public static function fake(Generator $faker): array;
/**
* @return array<int, array<string, mixed>>
*/
public static function asMeta(): array
{
return array_map(fn ($class) => $class::allMeta(), self::classNames());
}
/**
* @return array<int, class-string<self>>
*/
private static function classNames(): array
{
return collect(glob(base_path('app/Form/Fields/*.php')))
->filter(fn ($fieldClass) => preg_match('/[A-Za-z]Field\.php$/', $fieldClass) === 1)
->map(fn ($fieldClass) => str($fieldClass)->replace(base_path(''), '')->replace('/app', '/App')->replace('.php', '')->replace('/', '\\')->toString())
->values()
->toArray();
}
public static function classFromType(string $type): ?string
{
/** @var class-string<Field> */
$fieldClass = '\\App\\Form\\Fields\\' . $type;
if (!class_exists($fieldClass)) {
return null;
}
return $fieldClass;
}
/**
* @return mixed
*/
public function present()
{
return [
$this->key => $this->value,
$this->getDisplayAttribute() => $this->presentRaw(),
];
}
/**
* @return mixed
*/
public function presentRaw()
{
return $this->getPresenter()->present($this->value);
}
/**
* @return array<string, string>
*/
public static function metaAttributes(): array
{
return collect(static::meta())->mapWithKeys(fn ($meta) => [$meta['key'] => $meta['label']])->toArray();
}
/**
* @return array<string, mixed>
**/
public static function metaRules(): array
{
$result = [];
foreach (static::meta() as $meta) {
foreach ($meta['rules'] as $fieldName => $rules) {
$result[$fieldName] = $rules;
}
}
return $result;
}
public static function type(): string
{
return class_basename(static::class);
}
/**
* @return array<string, mixed>
*/
public static function allMeta(): array
{
return [
'id' => static::type(),
'name' => static::name(),
'default' => [
'name' => '',
'type' => static::type(),
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
'value' => static::default(),
'required' => false,
'nami_type' => null,
'for_members' => true,
'special_type' => null,
...collect(static::meta())->mapWithKeys(fn ($meta) => [$meta['key'] => $meta['default']])->toArray(),
],
];
}
public function getPresenter(): Presenter
{
return app(DefaultPresenter::class);
}
public function getDisplayAttribute(): string
{
return $this->key . '_display';
}
}

View File

@ -1,96 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\GroupPresenter;
use App\Form\Presenters\Presenter;
use App\Group;
use App\Group\Enums\Level;
use Faker\Generator;
use Illuminate\Validation\Rule;
class GroupField extends Field
{
public bool $required;
public ?string $parentField = null;
public ?int $parentGroup = null;
public static function name(): string
{
return 'Gruppierungs-Auswahl';
}
public static function meta(): array
{
return [
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
['key' => 'parent_field', 'default' => null, 'rules' => ['parent_field' => 'present|nullable|string'], 'label' => 'Übergeordnetes Feld'],
['key' => 'parent_group', 'default' => null, 'rules' => ['parent_group' => ['present', 'nullable', Rule::in(Group::pluck('id')->toArray())]], 'label' => 'Übergeordnete Gruppierung'],
];
}
public static function default(): string
{
return '';
}
public static function fake(Generator $faker): array
{
return [
'required' => $faker->boolean(),
'parent_field' => null,
'parent_group' => null,
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
$rules = [$this->required ? 'required' : 'nullable'];
$rules[] = 'integer';
if ($this->parentGroup) {
$rules[] = Rule::in(Group::find($this->parentGroup)->children()->pluck('id'));
}
if ($this->parentField && request()->input($this->parentField)) {
$rules[] = Rule::in(Group::find(request()->input($this->parentField))->children()->pluck('id'));
}
return [$this->key => $rules];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [$this->key => $this->name];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
public function getPresenter(): Presenter
{
return app(GroupPresenter::class);
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,131 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Data\FieldCollection;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\NamiPresenter;
use App\Form\Presenters\Presenter;
use App\Member\Member;
use Faker\Generator;
class NamiField extends Field
{
public static function name(): string
{
return 'NaMi-Mitglieder';
}
public static function meta(): array
{
return [];
}
/**
* @return array<string, mixed>
*/
public static function default(): array
{
return [];
}
public static function fake(Generator $faker): array
{
return [];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
$rules = [$this->key => 'present|array'];
$c = $form->getFields()->forMembers()->noNamiType()->noNamiField()
->map(fn ($field) => $field->getRegistrationRules($form))
->toArray();
foreach ($c as $field) {
foreach ($field as $ruleKey => $rule) {
$rules[$this->key . '.*.' . $ruleKey] = $rule;
}
}
return [
$this->key . '.*.id' => ['required', 'numeric', 'exists:members,mitgliedsnr'],
...$rules,
];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
$rules = [];
$inputMembers = request($this->key);
if (!is_array($inputMembers)) {
return [];
}
$c = $form->getFields()->noNamiField()->forMembers();
foreach ($c as $field) {
foreach ($field->getRegistrationRules($form) as $ruleKey => $rule) {
foreach ($inputMembers as $memberIndex => $inputMember) {
$message = $field->name . ' für ein Mitglied';
$rules = array_merge(
$rules,
str($ruleKey)->contains('*')
? collect(request($this->key . '.' . $memberIndex . '.' . $field->key))
->mapWithKeys(fn ($value, $key) => [$this->key . '.' . $memberIndex . '.' . str($ruleKey)->replace('*', $key) => $message])
->toArray()
: [$this->key . '.' . $memberIndex . '.' . $ruleKey => $message]
);
}
}
}
foreach ($inputMembers as $memberIndex => $inputMember) {
$rules[$this->key . '.' . $memberIndex . '.id'] = 'Mitglied Nr ' . $inputMember['id'];
}
return [
$this->key => $this->name,
...$rules,
];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
foreach ($input[$this->key] as $memberData) {
$member = Member::firstWhere(['mitgliedsnr' => $memberData['id']]);
$data = [];
foreach (FieldCollection::fromRequest($form, $memberData) as $field) {
$data[$field->key] = $field->namiType === null
? $field->value
: $field->namiType->getMemberAttribute($member);
}
$data[$this->key] = [];
$form->participants()->create(['data' => $data, 'mitgliedsnr' => $memberData['id'], 'parent_id' => $participant->id]);
}
}
public function getPresenter(): Presenter
{
return app(NamiPresenter::class);
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\Presenter;
use Faker\Generator;
use Illuminate\Validation\Rule;
class RadioField extends Field
{
public bool $required;
/** @var array<int, string> */
public array $options;
public static function name(): string
{
return 'Radio';
}
public static function meta(): array
{
return [
['key' => 'options', 'default' => [], 'rules' => ['options' => 'present|array', 'options.*' => 'required|string'], 'label' => 'Optionen'],
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
];
}
public static function default()
{
return null;
}
public static function fake(Generator $faker): array
{
return [
'options' => $faker->words(4),
'required' => $faker->boolean(),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
return [
$this->key => $this->required ? ['required', 'string', Rule::in($this->options)] : ['nullable', 'string', Rule::in($this->options)],
];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [$this->key => $this->name];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,68 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Faker\Generator;
class TextField extends Field
{
public bool $required;
public static function name(): string
{
return 'Text';
}
public static function meta(): array
{
return [
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
];
}
public static function default(): string
{
return '';
}
public static function fake(Generator $faker): array
{
return [
'required' => $faker->boolean(),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
return [$this->key => $this->required ? ['required', 'string'] : ['nullable', 'string']];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [$this->key => $this->name];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,70 +0,0 @@
<?php
namespace App\Form\Fields;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Faker\Generator;
class TextareaField extends Field
{
public bool $required;
public int $rows;
public static function name(): string
{
return 'Textarea';
}
public static function meta(): array
{
return [
['key' => 'rows', 'default' => 5, 'rules' => ['rows' => 'present|integer|gt:0'], 'label' => 'Zeilen'],
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
];
}
public static function default(): string
{
return '';
}
public static function fake(Generator $faker): array
{
return [
'rows' => $faker->numberBetween(5, 10),
'required' => $faker->boolean(),
];
}
/**
* @inheritdoc
*/
public function getRegistrationRules(Form $form): array
{
return [$this->key => $this->required ? ['required', 'string'] : ['nullable', 'string']];
}
/**
* @inheritdoc
*/
public function getRegistrationAttributes(Form $form): array
{
return [$this->key => $this->name];
}
/**
* @inheritdoc
*/
public function getRegistrationMessages(Form $form): array
{
return [];
}
/**
* @inheritdoc
*/
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
}

View File

@ -1,65 +0,0 @@
<?php
namespace App\Form\Mails;
use App\Form\Data\FormConfigData;
use App\Form\Models\Participant;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ConfirmRegistrationMail extends Mailable
{
use Queueable, SerializesModels;
public string $fullname;
public FormConfigData $config;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(public Participant $participant)
{
$this->fullname = $participant->getFields()->getFullname();
$this->config = $participant->getConfig();
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: 'Deine Anmeldung zu ' . $this->participant->form->name,
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
markdown: 'mail.form.confirm-registration',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, mixed>
*/
public function attachments()
{
return [];
}
}

View File

@ -1,144 +0,0 @@
<?php
namespace App\Form\Models;
use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData;
use App\Form\Fields\Field;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Laravel\Scout\Searchable;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Zoomyboy\MedialibraryHelper\DefersUploads;
class Form extends Model implements HasMedia
{
use HasFactory;
use Sluggable;
use InteractsWithMedia;
use DefersUploads;
use Searchable;
public $guarded = [];
public $casts = [
'config' => FormConfigData::class,
'meta' => 'json',
'description' => 'json',
];
/** @var array<int, string> */
public $dates = ['from', 'to', 'registration_from', 'registration_until'];
/**
* @return SluggableConfig
*/
public function sluggable(): array
{
return [
'slug' => ['source' => ['name']],
];
}
/**
* @return HasMany<Participant>
*/
public function participants(): HasMany
{
return $this->hasMany(Participant::class);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('headerImage')
->singleFile()
->maxWidth(fn () => 500)
->forceFileName(fn (Form $model, string $name) => $model->slug)
->registerMediaConversions(function (Media $media) {
$this->addMediaConversion('square')->fit(Manipulations::FIT_CROP, 400, 400);
});
}
/**
* @return array<string, mixed>
*/
public function getRegistrationRules(): array
{
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationRules($this),
], []);
}
/**
* @return array<string, mixed>
*/
public function getRegistrationMessages(): array
{
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationMessages($this),
], []);
}
/**
* @return array<string, mixed>
*/
public function getRegistrationAttributes(): array
{
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationAttributes($this),
], []);
}
public function getFields(): FieldCollection
{
return $this->config->fields();
}
// --------------------------------- Searching ---------------------------------
// *****************************************************************************
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray()
{
return [
'from' => $this->from->timestamp,
'to' => $this->to->timestamp,
'name' => $this->name,
];
}
public static function booted(): void
{
static::saving(function (self $model) {
if (is_null($model->meta)) {
$model->setAttribute('meta', [
'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null,
'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null,
]);
}
if (is_array(data_get($model->meta, 'active_columns'))) {
$model->setAttribute('meta', [
...$model->meta,
'active_columns' => array_values(array_intersect($model->getFields()->pluck('key')->toArray(), $model->meta['active_columns'])),
]);
}
});
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Form\Models;
use App\Form\Data\FormConfigData;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property FormConfigData $config
*/
class Formtemplate extends Model
{
use HasFactory;
public $guarded = [];
public $casts = [
'config' => FormConfigData::class,
];
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Form\Models;
use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData;
use App\Form\Mails\ConfirmRegistrationMail;
use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Mail;
class Participant extends Model
{
use HasFactory;
public $guarded = [];
public $casts = [
'data' => 'json',
];
/**
* @return BelongsTo<Form, self>
*/
public function form(): BelongsTo
{
return $this->belongsTo(Form::class);
}
/**
* @return HasMany<self>
*/
public function children(): HasMany
{
return $this->hasMany(self::class, 'parent_id');
}
/**
* @param Builder<self> $query
* @return Builder<self>
*/
public function scopeWithFilter(Builder $query, ParticipantFilterScope $filter): Builder
{
return $filter->apply($query);
}
public function getFields(): FieldCollection
{
return FieldCollection::fromRequest($this->form, $this->data);
}
public function getConfig(): FormConfigData
{
return tap($this->form->config, function ($config) {
$config->sections->each(function ($section) {
$section->fields->each(function ($field) {
$field->value = $this->getFields()->find($field)->value;
});
});
});
}
public function sendConfirmationMail(): void
{
if (!$this->getFields()->getMailRecipient()) {
return;
}
Mail::to($this->getFields()->getMailRecipient())->queue(new ConfirmRegistrationMail($this));
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Form\Policies;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Spatie\MediaLibrary\HasMedia;
class FormPolicy
{
use HandlesAuthorization;
public function listMedia(User $user, HasMedia $model): bool
{
return true;
}
public function storeMedia(User $user, ?HasMedia $model, ?string $collection = null): bool
{
return true;
}
public function updateMedia(User $user, HasMedia $model, string $collection): bool
{
return true;
}
public function destroyMedia(User $user, HasMedia $model, string $collection): bool
{
return true;
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Form\Presenters;
use Carbon\Carbon;
class DatePresenter extends Presenter
{
public function present($value): string
{
return $value ? Carbon::parse($value)->format('d.m.Y') : '';
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Form\Presenters;
class DefaultPresenter extends Presenter
{
/**
* @param mixed $value
*/
public function present($value): string
{
return ((string) $value) ?: '';
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Form\Presenters;
class EnumPresenter extends Presenter
{
/**
* @param ?array<int, string> $value
*/
public function present($value): string
{
return is_array($value)
? implode(', ', $value)
: '';
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Form\Presenters;
use App\Group;
use Carbon\Carbon;
class GroupPresenter extends Presenter
{
/**
* @param ?int $value
*/
public function present($value): string
{
if (!$value) {
return '';
}
return Group::find($value)?->display() ?: '';
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Form\Presenters;
class NamiPresenter extends Presenter
{
/**
* @param ?array<int, array{id: int}> $value
*/
public function present($value): string
{
return collect(array_column($value, 'id'))->implode(', ');
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Form\Presenters;
abstract class Presenter
{
/** @var mixed */
public $value;
/**
* @param mixed $value
*/
abstract public function present($value): string;
}

View File

@ -1,50 +0,0 @@
<?php
namespace App\Form\Resources;
use App\Form\Models\Form;
use App\Lib\HasMeta;
use App\Subactivity;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Form
*/
class FormApiResource extends JsonResource
{
use HasMeta;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'from_human' => $this->from->format('d.m.Y'),
'to_human' => $this->to->format('d.m.Y'),
'excerpt' => $this->excerpt,
'description' => $this->description,
'config' => $this->config,
'slug' => $this->slug,
'dates' => $this->from->equalTo($this->to) ? $this->from->format('d.m.Y') : $this->from->format('d.m.Y') . ' - ' . $this->to->format('d.m.Y'),
'image' => $this->getMedia('headerImage')->first()->getFullUrl('square'),
];
}
/**
* @return array<string, mixed>
*/
public static function meta(): array
{
return [
'base_url' => url(''),
'agegroups' => Subactivity::remote()->where('is_age_group', true)->get()
->map(fn ($subactivity) => ['id' => $subactivity->nami_id, 'name' => $subactivity->name]),
];
}
}

View File

@ -1,92 +0,0 @@
<?php
namespace App\Form\Resources;
use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType;
use App\Form\Fields\Field;
use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form;
use App\Form\Models\Formtemplate;
use App\Group;
use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Form
*/
class FormResource extends JsonResource
{
use HasMeta;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'from_human' => $this->from->format('d.m.Y'),
'to_human' => $this->to->format('d.m.Y'),
'from' => $this->from->format('Y-m-d'),
'to' => $this->to->format('Y-m-d'),
'excerpt' => $this->excerpt,
'description' => $this->description,
'mail_top' => $this->mail_top,
'mail_bottom' => $this->mail_bottom,
'registration_from' => $this->registration_from?->format('Y-m-d H:i:s'),
'registration_until' => $this->registration_until?->format('Y-m-d H:i:s'),
'config' => $this->config,
'participants_count' => $this->participants_count,
'links' => [
'participant_index' => route('form.participant.index', ['form' => $this->getModel()]),
'update' => route('form.update', ['form' => $this->getModel()]),
'destroy' => route('form.destroy', ['form' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function meta(): array
{
return [
'base_url' => url(''),
'groups' => Group::forSelect(),
'fields' => Field::asMeta(),
'filter' => FormFilterScope::fromRequest(request()->input('filter', '')),
'links' => [
'store' => route('form.store'),
'formtemplate_index' => route('formtemplate.index'),
],
'templates' => FormtemplateResource::collection(Formtemplate::get()),
'namiTypes' => NamiType::forSelect(),
'specialTypes' => SpecialType::forSelect(),
'default' => [
'description' => [],
'name' => '',
'excerpt' => '',
'from' => null,
'to' => null,
'registration_from' => null,
'registration_until' => null,
'mail_top' => null,
'mail_bottom' => null,
'config' => null,
'header_image' => null,
'id' => null,
],
'section_default' => [
'name' => '',
'intro' => '',
'fields' => [],
]
];
}
}

View File

@ -1,66 +0,0 @@
<?php
namespace App\Form\Resources;
use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType;
use App\Form\Fields\Field;
use App\Form\Models\Formtemplate;
use App\Group;
use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Formtemplate
*/
class FormtemplateResource extends JsonResource
{
use HasMeta;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
...parent::toArray($request),
'links' => [
'update' => route('formtemplate.update', ['formtemplate' => $this->getModel()]),
'destroy' => route('formtemplate.destroy', ['formtemplate' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function meta(): array
{
return [
'base_url' => url(''),
'groups' => Group::forSelect(),
'fields' => Field::asMeta(),
'namiTypes' => NamiType::forSelect(),
'specialTypes' => SpecialType::forSelect(),
'links' => [
'store' => route('formtemplate.store'),
'form_index' => route('form.index'),
],
'default' => [
'name' => '',
'config' => [
'sections' => [],
]
],
'section_default' => [
'name' => '',
'intro' => '',
'fields' => [],
]
];
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Form\Resources;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Participant
*/
class ParticipantResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return $this->getModel()->getFields()->present();
}
/**
* @return array<string, mixed>
*/
public static function meta(Form $form): array
{
return [
'form_meta' => $form->meta,
'links' => [
'update_form_meta' => route('form.update-meta', ['form' => $form]),
],
'columns' => $form->getFields()
->map(fn ($field) => [
'name' => $field->name,
'base_type' => class_basename($field),
'id' => $field->key,
'display_attribute' => $field->getDisplayAttribute(),
])
];
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Form\Scopes;
use Laravel\Scout\Builder;
use App\Form\Models\Form;
use App\Lib\ScoutFilter;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/**
* @extends ScoutFilter<Form>
*/
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class FormFilterScope extends ScoutFilter
{
public function __construct(
public ?string $search = '',
public bool $past = false,
) {
}
public function getQuery(): Builder
{
$this->search = $this->search ?: '';
return Form::search($this->search, function ($engine, string $query, array $options) {
$options['sort'] = ['from:asc'];
$filters = collect([]);
if ($this->past === false) {
$filters->push('to > ' . now()->timestamp);
}
$options['filter'] = $filters->implode(' AND ');
return $engine->search($query, $options);
});
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Form\Scopes;
use App\Form\Models\Participant;
use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/**
* @extends Filter<Participant>
*/
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class ParticipantFilterScope extends Filter
{
public function __construct(
public ?int $parent = null,
) {
}
/**
* @inheritdoc
*/
public function apply(Builder $query): Builder
{
if ($this->parent === -1) {
$query = $query->whereNull('parent_id');
}
if (!is_null($this->parent) && $this->parent > 0) {
$query = $query->where('parent_id', $this->parent);
}
return $query;
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Form\Transformers;
use App\Form\Fields\Field;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Transformers\Transformer;
class CollectionTransformer implements Transformer
{
public function __construct(public string $target)
{
}
/**
* @param Collection<int, Field> $value
* @return array<string, mixed>
*/
public function transform(DataProperty $property, mixed $value): mixed
{
return $value->toArray();
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Form\Transformers;
use App\Form\Fields\Field;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Transformers\Transformer;
class FieldCollectionTransformer implements Transformer
{
/**
* @param Collection<int, Field> $value
* @return array<string, mixed>
*/
public function transform(DataProperty $property, mixed $value): mixed
{
return $value->map(fn ($field) => [
...$field->toArray(),
'type' => class_basename($field),
])->toArray();
}
}

View File

@ -45,25 +45,4 @@ class Group extends Model
}
});
}
/**
* @return array<int, array{id: int, name: string}>
*/
public static function forSelect(?self $parent = null, string $prefix = ''): array
{
$result = self::where('parent_id', $parent ? $parent->id : null)->withCount('children')->get();
return $result
->reduce(
fn ($before, $group) => $before->concat([['id' => $group->id, 'name' => $prefix . ($group->display())]])
->concat($group->children_count > 0 ? self::forSelect($group, $prefix . '-- ') : []),
collect([])
)
->toArray();
}
public function display(): string
{
return $this->inner_name ?: $this->name;
}
}

View File

@ -18,9 +18,9 @@ class NamiSearchAction
*
* @return LengthAwarePaginator<MemberEntry>
*/
public function handle(Api $api, int $page, array $params, int $perPage = 10): LengthAwarePaginator
public function handle(Api $api, int $page, array $params): LengthAwarePaginator
{
return $api->pageSearch($params, $page, $perPage);
return $api->pageSearch($params, $page, 10);
}
/**

View File

@ -2,8 +2,8 @@
namespace App\Lib;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Data;
/**
@ -14,10 +14,10 @@ abstract class Filter extends Data
{
/**
* @param Builder<T> $query
* @return Builder<T>
* @return Builder
*/
abstract public function apply(Builder $query): Builder;
abstract public function getQuery(): Builder;
protected Builder $query;
/**
* @param array<string, mixed>|string|null $request

View File

@ -1,41 +0,0 @@
<?php
namespace App\Lib;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Data;
/**
* @template T of Model
* @property Builder $query
*/
abstract class ScoutFilter extends Data
{
/**
* @return Builder
*/
abstract public function getQuery(): Builder;
protected Builder $query;
/**
* @param array<string, mixed>|string|null $request
*/
public static function fromRequest(array|string|null $request = null): static
{
$payload = is_string($request)
? json_decode(rawurldecode(base64_decode($request)), true)
: $request;
return static::fromPost($payload);
}
/**
* @param array<string, mixed> $post
*/
public static function fromPost(?array $post = null): static
{
return static::withoutMagicalCreationFrom($post ?: []);
}
}

View File

@ -5,8 +5,8 @@ namespace App\Member;
use App\Activity;
use App\Group;
use App\Invoice\BillKind;
use App\Lib\Filter;
use App\Subactivity;
use App\Lib\ScoutFilter;
use Illuminate\Support\Collection;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Attributes\MapInputName;
@ -14,11 +14,11 @@ use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/**
* @extends ScoutFilter<Member>
* @extends Filter<Member>
*/
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class FilterScope extends ScoutFilter
class FilterScope extends Filter
{
/**
* @param array<int, int> $activityIds

View File

@ -93,7 +93,7 @@ class Member extends Model implements Geolocatable
}
/**
* @return SluggableConfig
* @return array<string, array{source: array<int, string>}>
*/
public function sluggable(): array
{

View File

@ -7,14 +7,12 @@ enum Module: string
case BILL = 'bill';
case COURSE = 'course';
case EVENT = 'event';
public function title(): string
{
return match ($this) {
static::BILL => 'Zahlungs-Management',
static::COURSE => 'Ausbildung',
static::EVENT => 'Veranstaltungen',
};
}

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use App\Form\Models\Form;
use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\MailmanType;
use Illuminate\Http\RedirectResponse;
@ -34,8 +33,6 @@ class AppServiceProvider extends ServiceProvider
LocalType::class,
MailmanType::class,
]));
app()->extend('media-library-helpers', fn ($p) => $p->put('form', Form::class));
}
/**

View File

@ -2,8 +2,6 @@
namespace App\Providers;
use App\Form\Models\Form;
use App\Form\Policies\FormPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
@ -12,10 +10,10 @@ class AuthServiceProvider extends ServiceProvider
/**
* The policy mappings for the application.
*
* @var array<class-string, class-string>
* @var array
*/
protected $policies = [
Form::class => FormPolicy::class,
// 'App\Model' => 'App\Policies\ModelPolicy',
];
/**

View File

@ -57,8 +57,5 @@ class RouteServiceProvider extends ServiceProvider
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('api')
->prefix('remote')
->group(base_path('routes/remote.php'));
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Remote\Actions;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Crypt;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Nami;
class LoginAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'mglnr' => 'required|numeric',
'password' => 'required|string',
];
}
public function handle(ActionRequest $request): JsonResponse
{
Nami::freshLogin($request->mglnr, $request->password);
return response()->json([
'access_token' => Crypt::encryptString(json_encode(['mglnr' => $request->mglnr, 'password' => $request->password])),
]);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Remote\Actions;
use App\Initialize\Actions\NamiSearchAction;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Crypt;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Nami;
class SearchAction
{
use AsAction;
/**
* @return LengthAwarePaginator<array<string, mixed>>
*/
public function handle(ActionRequest $request): LengthAwarePaginator
{
$token = str($request->header('Authorization'))->replace('Bearer ', '')->toString();
$credentials = json_decode(Crypt::decryptString($token));
$api = Nami::login($credentials->mglnr, $credentials->password);
$results = NamiSearchAction::run($api, $request->input('page', 1), $request->except('page'), 50);
$results->transform(fn ($member) => ['id' => $member->memberId, 'name' => $member->firstname . ' ' . $member->lastname]);
return $results;
}
}

View File

@ -20,13 +20,6 @@
"options": {
"symlink": true
}
},
{
"type": "path",
"url": "./packages/medialibrary-helper",
"options": {
"symlink": true
}
}
],
"license": "MIT",
@ -60,12 +53,12 @@
"zoomyboy/laravel-nami": "dev-master",
"zoomyboy/osm": "1.0.3",
"zoomyboy/phone": "^1.0",
"zoomyboy/tex": "dev-main as 1.0",
"zoomyboy/medialibrary-helper": "dev-master as 1.0"
"zoomyboy/tex": "dev-main as 1.0"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"laravel/envoy": "^2.8",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4",
"larastan/larastan": "^2.0",
"orchestra/testbench": "^7.0",

2983
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,267 +0,0 @@
<?php
return [
/*
* The disk on which to store added files and derived images by default. Choose
* one or more of the disks you've configured in config/filesystems.php.
*/
'disk_name' => env('MEDIA_DISK', 'public'),
/*
* The maximum file size of an item in bytes.
* Adding a larger file will result in an exception.
*/
'max_file_size' => 1024 * 1024 * 10, // 10MB
/*
* This queue connection will be used to generate derived and responsive images.
* Leave empty to use the default queue connection.
*/
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
/*
* This queue will be used to generate derived and responsive images.
* Leave empty to use the default queue.
*/
'queue_name' => '',
/*
* By default all conversions will be performed on a queue.
*/
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
/*
* The fully qualified class name of the media model.
*/
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class,
/*
* When enabled, media collections will be serialised using the default
* laravel model serialization behaviour.
*
* Keep this option disabled if using Media Library Pro components (https://medialibrary.pro)
*/
'use_default_collection_serialization' => false,
/*
* The fully qualified class name of the model used for temporary uploads.
*
* This model is only used in Media Library Pro (https://medialibrary.pro)
*/
'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class,
/*
* When enabled, Media Library Pro will only process temporary uploads that were uploaded
* in the same session. You can opt to disable this for stateless usage of
* the pro components.
*/
'enable_temporary_uploads_session_affinity' => true,
/*
* When enabled, Media Library pro will generate thumbnails for uploaded file.
*/
'generate_thumbnails_for_temporary_uploads' => true,
/*
* This is the class that is responsible for naming generated files.
*/
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class,
/*
* The class that contains the strategy for determining a media file's path.
*/
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class,
/*
* The class that contains the strategy for determining how to remove files.
*/
'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class,
/*
* Here you can specify which path generator should be used for the given class.
*/
'custom_path_generators' => [
// Model::class => PathGenerator::class
// or
// 'model_morph_alias' => PathGenerator::class
],
/*
* When urls to files get generated, this class will be called. Use the default
* if your files are stored locally above the site root or on s3.
*/
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class,
/*
* Moves media on updating to keep path consistent. Enable it only with a custom
* PathGenerator that uses, for example, the media UUID.
*/
'moves_media_on_update' => false,
/*
* Whether to activate versioning when urls to files get generated.
* When activated, this attaches a ?v=xx query string to the URL.
*/
'version_urls' => false,
/*
* The media library will try to optimize all converted images by removing
* metadata and applying a little bit of compression. These are
* the optimizers that will be used by default.
*/
'image_optimizers' => [
Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [
'-m85', // set maximum quality to 85%
'--force', // ensure that progressive generation is always done also if a little bigger
'--strip-all', // this strips out all text information such as comments and EXIF data
'--all-progressive', // this will make sure the resulting image is a progressive one
],
Spatie\ImageOptimizer\Optimizers\Pngquant::class => [
'--force', // required parameter for this package
],
Spatie\ImageOptimizer\Optimizers\Optipng::class => [
'-i0', // this will result in a non-interlaced, progressive scanned image
'-o2', // this set the optimization level to two (multiple IDAT compression trials)
'-quiet', // required parameter for this package
],
Spatie\ImageOptimizer\Optimizers\Svgo::class => [
'--disable=cleanupIDs', // disabling because it is known to cause troubles
],
Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [
'-b', // required parameter for this package
'-O3', // this produces the slowest but best results
],
Spatie\ImageOptimizer\Optimizers\Cwebp::class => [
'-m 6', // for the slowest compression method in order to get the best compression.
'-pass 10', // for maximizing the amount of analysis pass.
'-mt', // multithreading for some speed improvements.
'-q 90', //quality factor that brings the least noticeable changes.
],
Spatie\ImageOptimizer\Optimizers\Avifenc::class => [
'-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63).
'-j all', // number of jobs (worker threads, "all" uses all available cores).
'--min 0', // min quantizer for color (0-63).
'--max 63', // max quantizer for color (0-63).
'--minalpha 0', // min quantizer for alpha (0-63).
'--maxalpha 63', // max quantizer for alpha (0-63).
'-a end-usage=q', // rate control mode set to Constant Quality mode.
'-a tune=ssim', // SSIM as tune the encoder for distortion metric.
],
],
/*
* These generators will be used to create an image of media files.
*/
'image_generators' => [
Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class,
],
/*
* The path where to store temporary files while performing image conversions.
* If set to null, storage_path('media-library/temp') will be used.
*/
'temporary_directory_path' => null,
/*
* The engine that should perform the image conversions.
* Should be either `gd` or `imagick`.
*/
'image_driver' => env('IMAGE_DRIVER', 'gd'),
/*
* FFMPEG & FFProbe binaries paths, only used if you try to generate video
* thumbnails and have installed the php-ffmpeg/php-ffmpeg composer
* dependency.
*/
'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
/*
* Here you can override the class names of the jobs used by this package. Make sure
* your custom jobs extend the ones provided by the package.
*/
'jobs' => [
'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class,
'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class,
],
/*
* When using the addMediaFromUrl method you may want to replace the default downloader.
* This is particularly useful when the url of the image is behind a firewall and
* need to add additional flags, possibly using curl.
*/
'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class,
'remote' => [
/*
* Any extra headers that should be included when uploading media to
* a remote disk. Even though supported headers may vary between
* different drivers, a sensible default has been provided.
*
* Supported by S3: CacheControl, Expires, StorageClass,
* ServerSideEncryption, Metadata, ACL, ContentEncoding
*/
'extra_headers' => [
'CacheControl' => 'max-age=604800',
],
],
'responsive_images' => [
/*
* This class is responsible for calculating the target widths of the responsive
* images. By default we optimize for filesize and create variations that each are 30%
* smaller than the previous one. More info in the documentation.
*
* https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images
*/
'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class,
/*
* By default rendering media to a responsive image will add some javascript and a tiny placeholder.
* This ensures that the browser can already determine the correct layout.
*/
'use_tiny_placeholders' => true,
/*
* This class will generate the tiny placeholder used for progressive image loading. By default
* the media library will use a tiny blurred jpg image.
*/
'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class,
],
/*
* When enabling this option, a route will be registered that will enable
* the Media Library Pro Vue and React components to move uploaded files
* in a S3 bucket to their right place.
*/
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
/*
* When converting Media instances to response the media library will add
* a `loading` attribute to the `img` tag. Here you can set the default
* value of that attribute.
*
* Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction.
*
* More info: https://css-tricks.com/native-lazy-loading/
*/
'default_loading_attribute_value' => null,
/*
* You can specify a prefix for that is used for storing all media.
* If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory.
*/
'prefix' => env('MEDIA_PREFIX', ''),
/*
* The temp disk for deferred media data.
* Only used by the medialibrary-helper package.
*/
'temp_disk' => 'temp',
];

View File

@ -1,6 +1,5 @@
<?php
use App\Form\Models\Form;
use App\Member\Member;
return [
@ -141,12 +140,6 @@ return [
'searchableAttributes' => ['fullname', 'address'],
'sortableAttributes' => ['lastname', 'firstname'],
'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id'],
],
Form::class => [
'filterableAttributes' => ['to'],
'searchableAttributes' => ['name'],
'sortableAttributes' => ['from',],
'displayedAttributes' => ['from', 'name', 'id', 'to'],
]
],
],

View File

@ -1,78 +0,0 @@
<?php
namespace Database\Factories\Form\Models;
use App\Form\Models\Form;
use Database\Factories\Traits\FakesMedia;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Feature\Form\FormtemplateFieldRequest;
use Tests\Feature\Form\FormtemplateSectionRequest;
/**
* @extends Factory<Form>
* @method self name(string $name)
* @method self from(string $from)
* @method self to(string $to)
* @method self mailTop(string $content)
* @method self mailBottom(string $content)
* @method self excerpt(string $excerpt)
* @method self description(string $description)
* @method self registrationFrom(string|null $date)
* @method self registrationUntil(string|null $date)
*/
class FormFactory extends Factory
{
use FakesMedia;
/**
* The name of the factory's corresponding model.
*
* @var class-string<Form>
*/
protected $model = Form::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->words(4, true),
'description' => $this->faker->text(),
'excerpt' => $this->faker->words(10, true),
'config' => ['sections' => []],
'from' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'to' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'registration_from' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d H:i:s'),
'registration_until' => $this->faker->dateTimeBetween('now', '+2 weeks')->format('Y-m-d H:i:s'),
'mail_top' => $this->faker->text(),
'mail_bottom' => $this->faker->text(),
];
}
/**
* @param array<int, FormtemplateSectionRequest> $sections
*/
public function sections(array $sections): self
{
return $this->state(['config' => ['sections' => array_map(fn ($section) => $section->create(), $sections)]]);
}
/**
* @param array<int, FormtemplateFieldRequest> $fields
*/
public function fields(array $fields): self
{
return $this->sections([FormtemplateSectionRequest::new()->fields($fields)]);
}
/**
* @param mixed $parameters
*/
public function __call($method, $parameters): self
{
return $this->state([str($method)->snake()->toString() => $parameters[0]]);
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace Database\Factories\Form\Models;
use App\Form\Models\Formtemplate;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Feature\Form\FormtemplateSectionRequest;
/**
* @extends Factory<Formtemplate>
* @method self name(string $name)
*/
class FormtemplateFactory extends Factory
{
public $model = Formtemplate::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->words(4, true),
'config' => [
'sections' => [],
],
];
}
/**
* @param array<int, FormtemplateSectionRequest> $sections
*/
public function sections(array $sections): self
{
return $this->state(['config' => ['sections' => array_map(fn ($section) => $section->create(), $sections)]]);
}
/**
* @param mixed $parameters
*/
public function __call($method, $parameters): self
{
return $this->state([str($method)->snake()->toString() => $parameters[0]]);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace Database\Factories\Form\Models;
use App\Form\Models\Participant;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Feature\Form\FormtemplateSectionRequest;
/**
* @extends Factory<Participant>
*/
class ParticipantFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<Participant>
*/
protected $model = Participant::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'data' => [],
];
}
/**
* @param array<string, mixed> $data
*/
public function data(array $data): self
{
return $this->state(['data' => $data]);
}
public function nr(int $number): self
{
return $this->state(['mitgliedsnr' => $number]);
}
}

View File

@ -37,14 +37,4 @@ class GroupFactory extends Factory
{
return $this->state(['name' => $name]);
}
public function level(Level $level): self
{
return $this->state(['level' => $level]);
}
public function innerName(string $name): self
{
return $this->state(['inner_name' => $name]);
}
}

View File

@ -52,7 +52,7 @@ class MemberFactory extends Factory
: Nationality::factory()->create();
$subscription = Subscription::count()
? Subscription::get()->random()
: Subscription::factory()->forFee()->create();
: Subscription::factory()->for(Fee::factory())->create();
return $this
->for($country)

View File

@ -19,16 +19,10 @@ class SubscriptionFactory extends Factory
{
return [
'name' => $this->faker->word,
'fee_id' => Fee::factory()->createOne()->id,
];
}
public function forFee(?int $namiId = null): self
{
$namiId = $namiId ?: $this->faker->numberBetween(600, 800);
return $this->for(Fee::factory()->inNami($namiId));
}
public function name(string $name): self
{
return $this->state(['name' => $name]);

View File

@ -48,9 +48,9 @@ class SubactivityFactory extends Factory
return $this->state(['nami_id' => $namiId]);
}
public function ageGroup(bool $ageGroup = true): self
public function ageGroup(): self
{
return $this->state(['is_age_group' => $ageGroup]);
return $this->state(['is_age_group' => true]);
}
public function filterable(): self

View File

@ -1,24 +0,0 @@
<?php
namespace Database\Factories\Traits;
use Illuminate\Http\UploadedFile;
use Spatie\MediaLibrary\HasMedia;
trait FakesMedia
{
public function withImage(string $collection, string $filename): self
{
return $this->afterCreating(function (HasMedia $model) use ($filename, $collection) {
$pathinfo = pathinfo($filename);
UploadedFile::fake()->image($filename, 1000, 1000)->storeAs('media-library', $filename, 'temp');
$model->addMediaFromDisk('media-library/' . $filename, 'temp')
->usingName($pathinfo['filename'])
->usingFileName($pathinfo['basename'])
->toMediaCollection($collection);
});
}
}

View File

@ -1,51 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('formtemplates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('config');
$table->timestamps();
});
Schema::create('forms', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug');
$table->json('description');
$table->text('excerpt');
$table->json('config');
$table->date('from');
$table->date('to');
$table->dateTime('registration_from')->nullable();
$table->dateTime('registration_until')->nullable();
$table->text('mail_top')->nullable();
$table->text('mail_bottom')->nullable();
$table->json('meta');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('formtemplates');
Schema::dropIfExists('forms');
}
};

View File

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

View File

@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('participants', function (Blueprint $table) {
$table->id();
$table->json('data');
$table->foreignId('form_id');
$table->foreignId('parent_id')->nullable();
$table->string('mitgliedsnr')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('participants');
}
};

55
package-lock.json generated
View File

@ -5,10 +5,6 @@
"packages": {
"": {
"dependencies": {
"@editorjs/editorjs": "^2.29.0",
"@editorjs/header": "^2.8.1",
"@editorjs/nested-list": "^1.4.2",
"@editorjs/paragraph": "^2.11.3",
"@inertiajs/vue3": "^1.0.14",
"@tailwindcss/typography": "^0.5.10",
"@vitejs/plugin-vue": "^4.6.2",
@ -29,7 +25,6 @@
"wnumb": "^1.2.0"
},
"devDependencies": {
"accounting": "^0.4.1",
"autoprefixer": "^10.4.17",
"axios": "^1.6.6",
"dayjs": "^1.11.10",
@ -72,11 +67,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@codexteam/icons": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
"integrity": "sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA=="
},
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@ -95,45 +85,6 @@
"kuler": "^2.0.0"
}
},
"node_modules/@editorjs/editorjs": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.29.0.tgz",
"integrity": "sha512-w2BVboSHokMBd/cAOZn0UU328o3gSZ8XUvFPA2e9+ciIGKILiRSPB70kkNdmhHkuNS3q2px+vdaIFaywBl7wGA=="
},
"node_modules/@editorjs/header": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.8.1.tgz",
"integrity": "sha512-y0HVXRP7m2W617CWo3fsb5HhXmSLaRpb9GzFx0Vkp/HEm9Dz5YO1s8tC7R8JD3MskwoYh7V0hRFQt39io/r6hA==",
"dependencies": {
"@codexteam/icons": "^0.0.5"
}
},
"node_modules/@editorjs/nested-list": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@editorjs/nested-list/-/nested-list-1.4.2.tgz",
"integrity": "sha512-qb1dAoJ+bihqmlR3822TC2GuIxEjTCLTZsZVWNces3uJIZ+W4019G3IJKBt/MOOgz4Evzad/RvUEKwPCPe6YOQ==",
"dependencies": {
"@codexteam/icons": "^0.0.2"
}
},
"node_modules/@editorjs/nested-list/node_modules/@codexteam/icons": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.2.tgz",
"integrity": "sha512-KdeKj3TwaTHqM3IXd5YjeJP39PBUZTb+dtHjGlf5+b0VgsxYD4qzsZkb11lzopZbAuDsHaZJmAYQ8LFligIT6Q=="
},
"node_modules/@editorjs/paragraph": {
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/@editorjs/paragraph/-/paragraph-2.11.3.tgz",
"integrity": "sha512-ON72lhvhgWzPrq4VXpHUeut9bsFeJgVATDeL850FVToOwYHKvdsNpfu0VgxEodhkXgzU/IGl4FzdqC2wd3AJUQ==",
"dependencies": {
"@codexteam/icons": "^0.0.4"
}
},
"node_modules/@editorjs/paragraph/node_modules/@codexteam/icons": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.4.tgz",
"integrity": "sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q=="
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@ -1132,12 +1083,6 @@
"node": ">=10.0.0"
}
},
"node_modules/accounting": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/accounting/-/accounting-0.4.1.tgz",
"integrity": "sha512-RU6KY9Y5wllyaCNBo1W11ZOTnTHMMgOZkIwdOOs6W5ibMTp72i4xIbEA48djxVGqMNTUNbvrP/1nWg5Af5m2gQ==",
"dev": true
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",

View File

@ -12,7 +12,6 @@
"fix": "eslint \"resources/js/**/*.{js,vue}\" --fix"
},
"devDependencies": {
"accounting": "^0.4.1",
"autoprefixer": "^10.4.17",
"axios": "^1.6.6",
"dayjs": "^1.11.10",
@ -24,10 +23,6 @@
"vue-axios": "^3.5.2"
},
"dependencies": {
"@editorjs/editorjs": "^2.29.0",
"@editorjs/header": "^2.8.1",
"@editorjs/nested-list": "^1.4.2",
"@editorjs/paragraph": "^2.11.3",
"@inertiajs/vue3": "^1.0.14",
"@tailwindcss/typography": "^0.5.10",
"@vitejs/plugin-vue": "^4.6.2",

@ -1 +0,0 @@
Subproject commit 2e232a95fd84f6dcca80abe4d2ed37e1fac7e352

@ -1 +0,0 @@
Subproject commit 91e5cc3e3b6d7d0a8a3e2361514ec8c1ce9cb655

View File

@ -24,7 +24,6 @@ parameters:
ContributionApiRequestArray: 'array{dateFrom: string, dateUntil: string, zipLocation: string, country: int, eventName: string, member_data: array<int, ContributionMemberData>}'
MailgatewayCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string}'
MailgatewayParsedCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string, is_required: bool}'
SluggableConfig: 'array<string, array{source: array<int, string>}>'
ignoreErrors:
-
@ -137,6 +136,16 @@ parameters:
count: 1
path: app/Payment/SubscriptionResource.php
-
message: "#^PHPDoc type array of property App\\\\Providers\\\\AuthServiceProvider\\:\\:\\$policies is not covariant with PHPDoc type array\\<class\\-string, class\\-string\\> of overridden property Illuminate\\\\Foundation\\\\Support\\\\Providers\\\\AuthServiceProvider\\:\\:\\$policies\\.$#"
count: 1
path: app/Providers/AuthServiceProvider.php
-
message: "#^Property App\\\\Providers\\\\AuthServiceProvider\\:\\:\\$policies type has no value type specified in iterable type array\\.$#"
count: 1
path: app/Providers/AuthServiceProvider.php
-
message: "#^Method Database\\\\Factories\\\\ActivityFactory\\:\\:definition\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@ -532,18 +541,3 @@ parameters:
message: "#^Parameter \\#1 \\.\\.\\.\\$parts of method App\\\\Member\\\\FilterScope\\:\\:combinations\\(\\) expects array\\<int, int\\>, array\\<int, array\\<int, int\\>\\> given\\.$#"
count: 1
path: app/Member/FilterScope.php
-
message: "#^Call to function is_null\\(\\) with array will always evaluate to false\\.$#"
count: 1
path: app/Form/Models/Form.php
-
message: "#^Unable to resolve the template type TKey in call to function collect$#"
count: 1
path: app/Form/Fields/NamiField.php
-
message: "#^Unable to resolve the template type TValue in call to function collect$#"
count: 1
path: app/Form/Fields/NamiField.php

View File

@ -9,4 +9,3 @@
@import 'table';
@import 'bool';
@import 'form';
@import 'editor';

View File

@ -1,7 +1,3 @@
::selection {
@apply bg-blue-800;
}
.button-group > div {
padding-left: 0 !important;
padding-right: 0 !important;

View File

@ -1,83 +0,0 @@
.ce-inline-toolbar,
.codex-editor--narrow .ce-toolbox,
.ce-conversion-toolbar,
.ce-settings,
.ce-settings__button,
.ce-toolbar__settings-btn,
.cdx-button,
.ce-popover,
.ce-toolbar__plus:hover {
@apply bg-primary-700;
color: inherit;
}
.ce-inline-tool-input {
@apply bg-primary-700 text-primary-200 placeholder-primary-500;
}
.ce-block--selected {
@apply bg-gray-800;
}
h2.ce-header {
@apply font-semibold text-white text-xl mt-2;
}
h3.ce-header {
@apply font-semibold text-white text-lg mt-2;
}
h4.ce-header {
@apply font-semibold text-white mt-2;
}
.ce-block--selected .ce-block__content {
@apply bg-gray-800;
}
.ce-conversion-tool__icon,
.ce-popover-item__icon {
@apply bg-primary-800;
}
.ce-popover-item__title {
@apply text-primary-200;
}
.ce-popover-item:hover:not(.ce-popover-item--no-hover) {
@apply bg-primary-800;
}
.ce-inline-tool,
.ce-conversion-toolbar__label,
.ce-toolbox__button,
.cdx-settings-button,
.ce-toolbar__plus {
color: inherit;
}
.codex-editor ::selection {
@apply bg-blue-800;
}
.cdx-settings-button:hover,
.ce-settings__button:hover,
.ce-toolbox__button--active,
.ce-toolbox__button:hover,
.cdx-button:hover,
.ce-inline-toolbar__dropdown:hover,
.ce-inline-tool:hover,
.ce-popover__item:hover,
.ce-toolbar__settings-btn:hover {
background-color: #439a86;
color: inherit;
}
.cdx-notify--error {
background: #fb5d5d !important;
}
.cdx-notify__cross::after,
.cdx-notify__cross::before {
background: white;
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 503.664 503.664" style="enable-background:new 0 0 503.664 503.664" xml:space="preserve"><path d="M467.98 39.376H35.792C16.112 39.376.1 55.384.1 75.064L0 369.52c0 19.676 16.004 35.748 35.684 35.748h141.4v35.408h-27.612c-2.172 0-3.864 1.848-3.864 4.02v15.532c0 2.172 1.688 4.06 3.864 4.06h204c2.164 0 4.584-1.888 4.584-4.06v-15.532c0-2.172-2.42-4.02-4.584-4.02H326.58v-35.408h141.304c19.664 0 35.688-16.072 35.688-35.748l.092-294.424c.004-19.684-16.004-35.72-35.684-35.72zM251.34 377.524c-6.524 0-11.804-5.284-11.804-11.804 0-6.516 5.28-11.8 11.804-11.8 6.52 0 11.804 5.284 11.804 11.8 0 6.52-5.28 11.804-11.804 11.804zm220.812-50.944H31.512V70.848h440.64V326.58z"/></svg>

Before

Width:  |  Height:  |  Size: 723 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="124.813" height="124.813" style="enable-background:new 0 0 124.813 124.813" xml:space="preserve"><path d="m48.083 80.355-1.915 11.374a4.158 4.158 0 0 0 1.65 4.05 4.15 4.15 0 0 0 4.361.32l10.226-5.338L72.631 96.1a4.184 4.184 0 0 0 1.924.472c.859 0 1.716-.269 2.439-.792a4.152 4.152 0 0 0 1.651-4.05l-1.913-11.374 8.234-8.077a4.159 4.159 0 0 0 1.044-4.247 4.152 4.152 0 0 0-3.341-2.823l-11.41-1.692-5.139-10.329a4.147 4.147 0 0 0-3.716-2.303 4.16 4.16 0 0 0-3.718 2.303l-5.134 10.329-11.41 1.691a4.147 4.147 0 0 0-3.339 2.823 4.147 4.147 0 0 0 1.042 4.247l8.238 8.077z"/><path d="M111.443 13.269H98.378V6.022A6.022 6.022 0 0 0 92.355 0H91.4a6.021 6.021 0 0 0-6.021 6.022v7.247H39.282V6.022A6.022 6.022 0 0 0 33.261 0h-.956a6.021 6.021 0 0 0-6.021 6.022v7.247H13.371C6.538 13.269.977 18.828.977 25.663v86.757c0 6.831 5.561 12.394 12.394 12.394h98.073c6.832 0 12.394-5.562 12.394-12.394V25.663c-.001-6.835-5.563-12.394-12.395-12.394zm-1.617 97.534H14.988V43.268h94.838v67.535z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 503.604 503.604" style="enable-background:new 0 0 503.604 503.604" xml:space="preserve"><path d="M337.324 0H167.192c-28.924 0-53.5 23.584-53.5 52.5v398.664c0 28.916 24.056 52.44 52.98 52.44l170.412-.184c28.92 0 52.58-23.528 52.58-52.448l.248-398.5C389.908 23.452 366.364 0 337.324 0zM227.68 31.476h49.36c4.336 0 7.868 3.52 7.868 7.868 0 4.348-3.532 7.868-7.868 7.868h-49.36a7.865 7.865 0 0 1-7.868-7.868 7.865 7.865 0 0 1 7.868-7.868zm-29.66 2.504c2.916-2.912 8.224-2.952 11.136 0a7.973 7.973 0 0 1 2.324 5.588c0 2.048-.864 4.088-2.324 5.548-1.452 1.46-3.504 2.32-5.548 2.32-2.084 0-4.088-.86-5.588-2.32-1.452-1.456-2.28-3.5-2.28-5.548-.004-2.088.828-4.132 2.28-5.588zm52.752 454.028c-12.984 0-23.544-10.568-23.544-23.548 0-12.984 10.56-23.548 23.544-23.548s23.544 10.564 23.544 23.548c0 12.98-10.564 23.548-23.544 23.548zm114.716-63.1H141.232V74.756h224.256v350.152z"/></svg>

Before

Width:  |  Height:  |  Size: 929 B

View File

@ -1 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M51.2 353.28 0 512l158.72-51.2zm35.96-36.788L336.96 66.69 445.57 175.3l-249.8 249.802zM504.32 79.36 432.64 7.68c-10.24-10.24-25.6-10.24-35.84 0l-23.04 23.04 107.52 107.52 23.04-23.04c10.24-10.24 10.24-25.6 0-35.84z"/></svg>
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<polygon points="51.2,353.28 0,512 158.72,460.8 "/>
</g>
</g>
<g>
<g>
<rect x="89.73" y="169.097" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -95.8575 260.3719)" width="353.277" height="153.599"/>
</g>
</g>
<g>
<g>
<path d="M504.32,79.36L432.64,7.68c-10.24-10.24-25.6-10.24-35.84,0l-23.04,23.04l107.52,107.52l23.04-23.04 C514.56,104.96,514.56,89.6,504.32,79.36z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 660 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 503.592 503.592" style="enable-background:new 0 0 503.592 503.592" xml:space="preserve"><path d="M428.636 27.852C428.636 12.508 416.144 0 400.792 0H102.66C87.312 0 74.828 12.504 74.828 27.852l.132 447.892c0 15.352 12.488 27.848 27.832 27.848h298.136c15.352 0 27.836-12.496 27.836-27.848l-.128-447.892zm-196.276 3.62h38.884c4.36 0 7.868 3.52 7.868 7.868a7.863 7.863 0 0 1-7.868 7.872H232.36c-4.348 0-7.868-3.524-7.868-7.872s3.52-7.868 7.868-7.868zm-29.396 2.44c2.916-2.948 8.184-2.948 11.136 0 1.452 1.46 2.324 3.504 2.324 5.552 0 2.08-.872 4.084-2.324 5.544-1.46 1.496-3.504 2.324-5.584 2.324a7.747 7.747 0 0 1-5.552-2.324c-1.452-1.456-2.316-3.464-2.316-5.544 0-2.048.868-4.092 2.316-5.552zm47.252 454.216c-12.98 0-23.544-10.564-23.544-23.544 0-12.984 10.568-23.548 23.544-23.548 12.988 0 23.544 10.564 23.544 23.548s-10.552 23.544-23.544 23.544zM94.424 424.904V74.752h314.744v350.152H94.424z"/></svg>

Before

Width:  |  Height:  |  Size: 954 B

4
resources/js/app.js vendored
View File

@ -50,10 +50,6 @@ createInertiaApp({
requireModules(import.meta.glob('./components/form/*.vue'), app, 'f');
requireModules(import.meta.glob('./components/ui/*.vue'), app, 'ui');
requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), app, 'page');
app.component(
'f-singlefile',
defineAsyncComponent(() => import('!/medialibrary-helper/assets/components/SingleFile.vue'))
);
app.provide('axios', app.config.globalProperties.axios);
app.mount(el);

View File

@ -1,98 +0,0 @@
<template>
<div>
<span v-if="label" class="font-semibold text-gray-400" :class="labelClass(size)">{{ label }}<span v-show="required" class="text-red-800">&nbsp;*</span></span>
<div class="relative w-full h-full">
<div :id="id" :class="[defaultFieldClass, fieldClass(size)]"></div>
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
</div>
</div>
</div>
</template>
<script setup>
import {debounce} from 'lodash';
import {onMounted, ref} from 'vue';
import EditorJS from '@editorjs/editorjs';
import Header from '@editorjs/header';
import Paragraph from '@editorjs/paragraph';
import NestedList from '@editorjs/nested-list';
import useFieldSize from '../../composables/useFieldSize.js';
const emit = defineEmits(['update:modelValue']);
const {labelClass, fieldClass, defaultFieldClass} = useFieldSize();
const props = defineProps({
required: {
type: Boolean,
default: false,
},
size: {
default: null,
},
rows: {
default: function () {
return 4;
},
},
id: {
required: true,
},
hint: {
default: null,
},
modelValue: {
default: undefined,
},
label: {
default: false,
},
placeholder: {
default: '',
},
});
const editor = ref(null);
onMounted(async () => {
editor.value = new EditorJS({
placeholder: props.placeholder,
holder: props.id,
minHeight: 0,
defaultBlock: 'paragraph',
data: JSON.parse(JSON.stringify(props.modelValue)),
tools: {
paragraph: {
class: Paragraph,
shortcut: 'CTRL+P',
inlineToolbar: true,
config: {
preserveBlank: true,
placeholder: 'Absatz',
},
},
heading: {
class: Header,
shortcut: 'CTRL+H',
inlineToolbar: true,
config: {
placeholder: 'Überschrift',
levels: [2, 3, 4],
defaultLevel: 2,
},
},
list: {
class: NestedList,
shortcut: 'CTRL+L',
inlineToolbar: true,
},
},
onChange: debounce(async (api, event) => {
const data = await editor.value.save();
emit('update:modelValue', data);
}, 500),
});
await editor.value.isReady;
console.log('Editor is ready');
});
</script>

View File

@ -85,7 +85,7 @@ export default {
return Array.isArray(this.options)
? this.options
: map(this.options, (value, key) => {
return {name: value, id: key};
return { name: value, id: key };
});
},
},

View File

@ -5,20 +5,8 @@
<span v-show="required" class="text-red-800">&nbsp;*</span>
</span>
<div class="real-field-wrap size-sm" :class="sizes[size].field">
<input
:name="name"
:type="type"
:value="transformedValue"
:disabled="disabled"
:placeholder="placeholder"
:min="min"
:max="max"
@keypress="$emit('keypress', $event)"
@input="onInput"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
/>
<input :name="name" :type="type" :value="transformedValue" :disabled="disabled" :placeholder="placeholder"
@keypress="$emit('keypress', $event)" @input="onInput" @change="onChange" @focus="onFocus" @blur="onBlur" />
<div v-if="hint" class="info-wrap">
<div v-tooltip="hint">
<ui-sprite src="info-button" class="info-button"></ui-sprite>
@ -293,12 +281,6 @@ export default {
default: false,
type: Boolean,
},
min: {
default: () => '',
},
max: {
default: () => '',
},
name: {},
},
data: function () {
@ -342,7 +324,7 @@ export default {
},
created() {
if (typeof this.modelValue === 'undefined') {
this.$emit('update:modelValue', this.default === undefined ? '' : this.default);
this.$emit('input', this.default === undefined ? '' : this.default);
}
},
methods: {

View File

@ -1,26 +1,56 @@
<template>
<label class="flex flex-col">
<span v-if="label" class="font-semibold text-gray-400" :class="labelClass(size)">{{ label }}<span v-show="required" class="text-red-800">&nbsp;*</span></span>
<div class="relative w-full h-full">
<textarea :placeholder="placeholder" class="h-full w-full outline-none" :class="[defaultFieldClass, fieldClass(size)]" :rows="rows" @input="trigger" v-text="modelValue"></textarea>
<label class="flex flex-col relative">
<span
v-if="label && !inset"
class="font-semibold text-gray-400"
:class="{
'text-xs': size == 'sm',
'text-sm': size === null,
}"
>{{ label }}<span v-show="required" class="text-red-800">&nbsp;*</span></span
>
<span
v-if="label && inset"
class="absolute top-0 left-0 -mt-2 px-1 ml-3 inset-bg font-semibold text-gray-700"
:class="{
'text-xs': size == 'sm',
'text-sm': size === null,
}"
>{{ label }}<span v-show="required" class="text-red-800">&nbsp;*</span></span
>
<textarea
v-text="value"
@input="trigger"
:placeholder="placeholder"
class="h-full outline-none bg-gray-700 border-gray-600 border-solid"
:rows="rows"
:class="{
'rounded-lg text-sm border-2 p-2 text-gray-300': size === null,
'rounded-lg py-2 px-2 text-xs border-2 text-gray-300': size == 'sm',
}"
></textarea>
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
</div>
</div>
</label>
</template>
<script setup>
import useFieldSize from '../../composables/useFieldSize.js';
const emit = defineEmits(['update:modelValue']);
const {labelClass, fieldClass, defaultFieldClass} = useFieldSize();
const props = defineProps({
<script>
export default {
data: function () {
return {
focus: false,
};
},
props: {
required: {
type: Boolean,
default: false,
},
inset: {
default: false,
type: Boolean,
},
size: {
default: null,
},
@ -35,20 +65,40 @@ const props = defineProps({
hint: {
default: null,
},
modelValue: {
value: {
default: undefined,
},
mask: {
default: undefined,
},
label: {
default: false,
},
type: {
required: false,
default: function () {
return 'text';
},
},
placeholder: {
default: '',
},
});
function trigger(v) {
emit('update:modelValue', v.target.value);
}
if (typeof props.modelValue === 'undefined') {
emit('update:modelValue', '');
}
},
methods: {
trigger(v) {
this.$emit('input', v.target.value);
},
},
created() {
if (typeof this.value === 'undefined') {
this.$emit('input', '');
}
},
};
</script>
<style scope>
.inset-bg {
background: linear-gradient(to bottom, hsl(247.5, 66.7%, 97.6%) 0%, hsl(247.5, 66.7%, 97.6%) 41%, hsl(0deg 0% 100%) 41%, hsl(180deg 0% 100%) 100%);
}
</style>

View File

@ -1,23 +0,0 @@
<template>
<div class="p-6 bg-gray-700 border-r border-gray-600 flex-none w-maxc flex flex-col justify-between">
<div class="grid gap-1">
<a v-for="(item, index) in entries" :key="index" href="#" class="rounded py-1 px-3 text-gray-400"
:class="index === modelValue ? `bg-gray-600` : ''" @click.prevent="openMenu(index)" v-text="item.title"></a>
</div>
<slot name="bottom"></slot>
</div>
</template>
<script>
export default {
props: {
modelValue: {},
entries: {},
},
methods: {
openMenu(index) {
this.$emit('update:modelValue', index);
},
},
};
</script>

View File

@ -1,9 +1,6 @@
<template>
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6">
<div
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto"
:class="full ? 'h-full' : innerWidth"
>
<div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full overflow-auto" :class="full ? 'h-full' : innerWidth">
<div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6">
<slot name="actions"></slot>
<a href="#" @click.prevent="$emit('close')">
@ -11,19 +8,9 @@
</a>
</div>
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
<div class="text-primary-100 group is-popup grow flex flex-col">
<suspense>
<div>
<div class="text-primary-100 group is-popup">
<slot></slot>
</div>
<template #fallback>
<div class="flex flex-col items-center justify-center h-full">
<ui-spinner class="border-primary-400 w-32 h-32"></ui-spinner>
<div class="text-3xl mt-10">Lade </div>
</div>
</template>
</suspense>
</div>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More