diff --git a/app/Enums/ExpertLink.php b/app/Enums/ExpertLink.php new file mode 100644 index 0000000..58f06d9 --- /dev/null +++ b/app/Enums/ExpertLink.php @@ -0,0 +1,21 @@ +columns(1) + ->schema([ + Split::make([ + SpatieMediaLibraryFileUpload::make('avatar') + ->collection('avatar') + ->avatar() + ->grow(false), + + Grid::make() + ->schema([ + TextInput::make('name') + ->required() + ->maxLength(255) + ->columnSpanFull(), + + TextInput::make('title') + ->nullable() + ->maxLength(255), + + Select::make('country') + ->options(Country::options()) + ->enum(Country::class), + + Checkbox::make('enabled') + ->label('Enabled') + ->columnSpanFull(), + ]), + ]), + + Repeater::make('links') + ->columns(4) + ->schema([ + Select::make('type') + ->options(ExpertLink::options()) + ->enum(ExpertLink::class) + ->required(), + + TextInput::make('url') + ->url() + ->required() + ->columnSpan(3), + ]) + ->collapsible(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('order') + ->alignRight() + ->shrink(), + + ToggleColumn::make('enabled') + ->label('Enabled') + ->shrink(), + + SpatieMediaLibraryImageColumn::make('avatar') + ->collection('avatar') + ->conversion('thumb') + ->toggleable() + ->shrink(), + + TextColumn::make('name') + ->searchable() + ->sortable(), + + TextColumn::make('title') + ->searchable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('country') + ->badge() + ->formatStateUsing(fn (?Country $state) => $state?->label()) + ->toggleable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->defaultSort('order', 'asc') + ->reorderable('order'); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ManageExperts::route('/'), + ]; + } +} diff --git a/app/Filament/Resources/ExpertResource/Pages/ManageExperts.php b/app/Filament/Resources/ExpertResource/Pages/ManageExperts.php new file mode 100644 index 0000000..d8e63d6 --- /dev/null +++ b/app/Filament/Resources/ExpertResource/Pages/ManageExperts.php @@ -0,0 +1,21 @@ +schema([ + Checkbox::make('enabled') + ->label('Enabled') + ->columnSpanFull(), + + TextInput::make('name') + ->required(), + + TextInput::make('url') + ->url(), + + SpatieMediaLibraryFileUpload::make('logo') + ->collection('logo') + ->image() + ->columnSpanFull() + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('order') + ->alignRight() + ->shrink(), + + ToggleColumn::make('enabled') + ->label('Enabled') + ->shrink(), + + SpatieMediaLibraryImageColumn::make('logo') + ->collection('logo') + ->conversion('thumb') + ->toggleable() + ->shrink(), + + TextColumn::make('name') + ->sortable(), + + TextColumn::make('updated_at') + ->label('Last Updated') + ->toggleable() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->defaultSort('order', 'asc') + ->reorderable('order'); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ManageInstitutions::route('/'), + ]; + } +} diff --git a/app/Filament/Resources/InstitutionResource/Pages/ManageInstitutions.php b/app/Filament/Resources/InstitutionResource/Pages/ManageInstitutions.php new file mode 100644 index 0000000..26860f5 --- /dev/null +++ b/app/Filament/Resources/InstitutionResource/Pages/ManageInstitutions.php @@ -0,0 +1,21 @@ +columns([ - ToggleColumn::make('enabled') - ->label('Enabled') - ->shrink(), - TextColumn::make('order') ->alignRight() ->shrink(), + ToggleColumn::make('enabled') + ->label('Enabled') + ->shrink(), + TextColumn::make('key') ->label('Name') ->formatStateUsing(fn (?StatKey $state) => $state?->label()) @@ -69,6 +69,7 @@ public static function table(Table $table): Table TextColumn::make('updated_at') ->label('Last Updated') + ->toggleable() ->sortable(), ]) ->filters([ diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 42fe489..876f507 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -25,6 +25,8 @@ class UserResource extends Resource protected static ?string $navigationIcon = 'heroicon-o-users'; + protected static ?int $navigationSort = 31; + public static function getNavigationGroup(): ?string { return __('admin.navigation.admin'); @@ -67,7 +69,6 @@ public static function table(Table $table): Table SpatieMediaLibraryImageColumn::make('avatar') ->collection('avatar') ->conversion('thumb') - ->circular() ->shrink(), TextColumn::make('name') diff --git a/app/Livewire/Pages/Partners.php b/app/Livewire/Pages/Partners.php index 7e021a5..85e2bd4 100644 --- a/app/Livewire/Pages/Partners.php +++ b/app/Livewire/Pages/Partners.php @@ -4,6 +4,11 @@ namespace App\Livewire\Pages; +use App\Enums\ExpertLink; +use App\Models\Expert; +use App\Models\Institution; +use Illuminate\Support\Facades\Vite; +use Illuminate\Support\Str; use Livewire\Component; class Partners extends Component @@ -15,72 +20,85 @@ public function render() ->description(__('partners.intro_1')); return view('livewire.pages.partners', [ - 'partners' => $this->getPartners(), + 'institutions' => $this->getInstitutions(), 'experts' => $this->getExperts(), ]); } - protected function getPartners(): array + protected function getInstitutions(): array { + if (request()->boolean('alt')) { + return Institution::query() + ->where('enabled', true) + ->orderBy('order') + ->get() + ->map(fn (Institution $institution) => [ + 'name' => $institution->name, + 'logo' => $institution->getFirstMediaUrl('logo', 'large'), + 'url' => $institution->url, + ]) + ->all(); + } + return [ [ 'name' => 'Expert Forum (EFOR)', - 'logo' => 'EFOR.png', + 'logo' => Vite::asset('resources/images/partners/EFOR.png'), 'url' => 'https://expertforum.ro/en/', ], [ 'name' => 'Gender Concerns International', - 'logo' => 'Gender-Concerns-International.png', + 'logo' => Vite::asset('resources/images/partners/Gender-Concerns-International.png'), 'url' => 'https://www.genderconcerns.org/', ], [ 'name' => 'European Platform for Democratic Elections (EPDE)', - 'logo' => 'EPDE.png', + 'logo' => Vite::asset('resources/images/partners/EPDE.png'), 'url' => 'https://epde.org/', ], [ 'name' => 'Institute for Public Environment Development (IPED)', - 'logo' => 'IPED.png', + 'logo' => Vite::asset('resources/images/partners/IPED.png'), 'url' => 'https://iped.bg/en/', ], [ 'name' => 'Inter Alia', - 'logo' => 'Inter-Alia.png', + 'logo' => Vite::asset('resources/images/partners/Inter-Alia.png'), 'url' => 'https://interaliaproject.com/', ], [ 'name' => 'Political Accountability Foundation (PAF)', - 'logo' => 'PAF.png', + 'logo' => Vite::asset('resources/images/partners/PAF.png'), 'url' => 'https://odpowiedzialnapolityka.pl/', ], [ 'name' => 'European Exchange', - 'logo' => 'European-Exchange.png', + 'logo' => Vite::asset('resources/images/partners/European-Exchange.png'), 'url' => 'https://european-exchange.org/', ], [ 'name' => 'Danes je nov dan', - 'logo' => 'Danes-je-nov-dan.png', + 'logo' => Vite::asset('resources/images/partners/Danes-je-nov-dan.png'), 'url' => 'https://danesjenovdan.si/', ], [ 'name' => 'Committee for the Defence of Democracy (CDD)', - 'logo' => 'KOD.png', + 'logo' => Vite::asset('resources/images/partners/KOD.png'), 'url' => 'https://ruchkod.pl/', ], [ 'name' => 'Croatian Youth Network', - 'logo' => 'MMH.png', + 'logo' => Vite::asset('resources/images/partners/MMH.png'), 'url' => 'https://www.mmh.hr/', ], [ 'name' => 'Memo 98', - 'logo' => 'MEMO98.png', + 'logo' => Vite::asset('resources/images/partners/MEMO98.png'), 'url' => 'https://memo98.sk/', ], [ 'name' => 'Fórum 50 %', - 'logo' => 'Forum-50.png', + 'logo' => Vite::asset('resources/images/partners/Forum-50.png'), 'url' => 'https://padesatprocent.cz/cz/', ], ]; @@ -88,19 +106,45 @@ protected function getPartners(): array protected function getExperts(): array { + if (request()->boolean('alt')) { + return Expert::query() + ->where('enabled', true) + ->orderBy('order') + ->get() + ->map(fn (Expert $expert) => [ + 'name' => $expert->name, + 'title' => $expert->title, + 'country' => $expert->country?->label(), + 'avatar' => $expert->getFirstMediaUrl('avatar', 'large'), + 'links' => collect($expert->links) + ->map(fn (array $link) => [ + 'url' => $link['url'], + 'title' => Str::ucfirst($link['type']), + 'icon' => match (ExpertLink::tryFrom($link['type'])) { + ExpertLink::FACEBOOK => 'ri-facebook-box-fill', + ExpertLink::LINKEDIN => 'ri-linkedin-box-fill', + ExpertLink::TWITTER => 'ri-twitter-x-line', + ExpertLink::WEBSITE => 'ri-global-line', + default => 'ri-link', + }, + ]), + ]) + ->all(); + } + return [ [ 'name' => 'Maria Krause', - 'title' => __('countries.ro'), - 'avatar' => 'Maria-Krause.jpg', + 'country' => __('countries.ro'), + 'avatar' => Vite::asset('resources/images/experts/Maria-Krause.jpg'), 'links' => [ ], ], [ 'name' => 'Christoforos Christoforou', - 'title' => __('countries.cy'), - 'avatar' => 'Christoforos-Christoforou.png', + 'country' => __('countries.cy'), + 'avatar' => Vite::asset('resources/images/experts/Christoforos-Christoforou.png'), 'links' => [ [ 'icon' => 'ri-global-line', @@ -111,8 +155,8 @@ protected function getExperts(): array ], [ 'name' => 'Sabra Bano', - 'title' => __('countries.nl'), - 'avatar' => 'Sabra-Bano.jpg', + 'country' => __('countries.nl'), + 'avatar' => Vite::asset('resources/images/experts/Sabra-Bano.jpg'), 'links' => [ [ 'icon' => 'ri-global-line', diff --git a/app/Models/Expert.php b/app/Models/Expert.php new file mode 100644 index 0000000..015c8bc --- /dev/null +++ b/app/Models/Expert.php @@ -0,0 +1,56 @@ + Country::class, + 'links' => 'collection', + 'enabled' => 'boolean', + 'order' => 'integer', + ]; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('avatar') + ->useFallbackUrl(Vite::asset('resources/images/fallback-expert.png')) + ->singleFile() + ->registerMediaConversions(function () { + $this->addMediaConversion('thumb') + ->fit(Fit::Crop, 64, 64) + ->keepOriginalImageFormat() + ->optimize(); + + $this->addMediaConversion('large') + ->fit(Fit::Crop, 400, 400) + ->keepOriginalImageFormat() + ->optimize(); + }); + } +} diff --git a/app/Models/Institution.php b/app/Models/Institution.php new file mode 100644 index 0000000..8859b59 --- /dev/null +++ b/app/Models/Institution.php @@ -0,0 +1,51 @@ + 'boolean', + 'order' => 'integer', + ]; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('logo') + ->useFallbackUrl(Vite::asset('resources/images/fallback-institution.png')) + ->singleFile() + ->registerMediaConversions(function () { + $this->addMediaConversion('thumb') + ->fit(Fit::Crop, 54, 40) + ->keepOriginalImageFormat() + ->optimize(); + + $this->addMediaConversion('large') + ->fit(Fit::Crop, 387, 287) + ->keepOriginalImageFormat() + ->optimize(); + }); + } +} diff --git a/app/Policies/ExpertPolicy.php b/app/Policies/ExpertPolicy.php new file mode 100644 index 0000000..bbae141 --- /dev/null +++ b/app/Policies/ExpertPolicy.php @@ -0,0 +1,67 @@ +isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Expert $expert): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Expert $expert): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Expert $expert): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Expert $expert): bool + { + return $user->isAdmin(); + } +} diff --git a/app/Policies/InstitutionPolicy.php b/app/Policies/InstitutionPolicy.php new file mode 100644 index 0000000..f94cea4 --- /dev/null +++ b/app/Policies/InstitutionPolicy.php @@ -0,0 +1,67 @@ +isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Institution $institution): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Institution $institution): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Institution $institution): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Institution $institution): bool + { + return $user->isAdmin(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e93ed5b..a883251 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,8 @@ namespace App\Providers; use App\Models\ElectionDay; +use App\Models\Expert; +use App\Models\Institution; use App\Models\Media; use App\Models\Post; use App\Models\User; @@ -215,6 +217,8 @@ protected function enforceMorphMap(): void Relation::enforceMorphMap([ 'electionDay' => ElectionDay::class, 'media' => Media::class, + 'institution' => Institution::class, + 'expert' => Expert::class, 'post' => Post::class, 'user' => User::class, ]); diff --git a/database/factories/ExpertFactory.php b/database/factories/ExpertFactory.php new file mode 100644 index 0000000..8702abd --- /dev/null +++ b/database/factories/ExpertFactory.php @@ -0,0 +1,28 @@ + + */ +class ExpertFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'country' => fake()->randomElement(Country::values()), + 'enabled' => fake()->boolean(75), + ]; + } +} diff --git a/database/factories/InstitutionFactory.php b/database/factories/InstitutionFactory.php new file mode 100644 index 0000000..7bcc4ac --- /dev/null +++ b/database/factories/InstitutionFactory.php @@ -0,0 +1,27 @@ + + */ +class InstitutionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->company(), + 'url' => fake()->url(), + 'enabled' => fake()->boolean(75), + ]; + } +} diff --git a/database/migrations/2024_06_06_201311_create_institutions_table.php b/database/migrations/2024_06_06_201311_create_institutions_table.php new file mode 100644 index 0000000..d549d87 --- /dev/null +++ b/database/migrations/2024_06_06_201311_create_institutions_table.php @@ -0,0 +1,25 @@ +id(); + $table->timestamps(); + $table->string('name'); + $table->string('url')->nullable(); + $table->boolean('enabled')->default(false); + $table->unsignedTinyInteger('order')->default(0); + }); + } +}; diff --git a/database/migrations/2024_06_06_202725_create_experts_table.php b/database/migrations/2024_06_06_202725_create_experts_table.php new file mode 100644 index 0000000..cd4d514 --- /dev/null +++ b/database/migrations/2024_06_06_202725_create_experts_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + $table->string('name'); + $table->string('country')->nullable(); + $table->string('title')->nullable(); + $table->json('links')->nullable(); + $table->boolean('enabled')->default(false); + $table->unsignedTinyInteger('order')->default(0); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 0718331..d8e2d27 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -7,6 +7,8 @@ use App\Enums\StatKey; use App\Models\Author; use App\Models\ElectionDay; +use App\Models\Expert; +use App\Models\Institution; use App\Models\Post; use App\Models\Stat; use App\Models\User; @@ -23,6 +25,14 @@ public function run(): void ->admin() ->create(); + Institution::factory() + ->count(12) + ->create(); + + Expert::factory() + ->count(3) + ->create(); + $electionDays = ElectionDay::factory() ->count(4) ->create(); diff --git a/lang/en/admin.php b/lang/en/admin.php index 0e4f4ae..9261367 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -7,6 +7,7 @@ 'navigation' => [ 'newsfeed' => 'Newsfeed', 'admin' => 'Admin', + 'partners' => 'Partners', ], ]; diff --git a/resources/images/fallback-expert.png b/resources/images/fallback-expert.png new file mode 100644 index 0000000..77a81d4 Binary files /dev/null and b/resources/images/fallback-expert.png differ diff --git a/resources/images/fallback-institution.png b/resources/images/fallback-institution.png new file mode 100644 index 0000000..e2bcc37 Binary files /dev/null and b/resources/images/fallback-institution.png differ diff --git a/resources/views/components/partners/expert.blade.php b/resources/views/components/partners/expert.blade.php index 140e4b2..43f5bcc 100644 --- a/resources/views/components/partners/expert.blade.php +++ b/resources/views/components/partners/expert.blade.php @@ -1,8 +1,8 @@ -@props(['name' => null, 'title' => null, 'avatar' => null, 'links' => []]) +@props(['name' => null, 'title' => null, 'country' => null, 'avatar' => null, 'links' => []])
- {{ $name }}
@@ -11,9 +11,17 @@ class="object-contain w-full" /> {{ $name }} -

- {{ $title }} -

+ @if ($title) +

+ {{ $title }} +

+ @endif + + @if ($country) +

+ {{ $country }} +

+ @endif
@if (filled($links)) diff --git a/resources/views/components/partners/institution.blade.php b/resources/views/components/partners/institution.blade.php index 0ca51e5..8b18976 100644 --- a/resources/views/components/partners/institution.blade.php +++ b/resources/views/components/partners/institution.blade.php @@ -21,8 +21,7 @@
- {{ $name }} + {{ $name }}

diff --git a/resources/views/livewire/pages/partners.blade.php b/resources/views/livewire/pages/partners.blade.php index 2eb3fef..69bf86a 100644 --- a/resources/views/livewire/pages/partners.blade.php +++ b/resources/views/livewire/pages/partners.blade.php @@ -12,18 +12,18 @@ - @if (filled($partners)) + @if (filled($institutions))

{{ __('partners.institutional') }}

- @foreach ($partners as $partner) + @foreach ($institutions as $institution) + :name="data_get($institution, 'name')" + :logo="data_get($institution, 'logo')" + :url="data_get($institution, 'url')" /> @endforeach
@@ -40,6 +40,7 @@ @endforeach