diff --git a/app/DataTransferObjects/MapCoordinates.php b/app/DataTransferObjects/MapCoordinates.php index 6ffdd06e..274b8c26 100644 --- a/app/DataTransferObjects/MapCoordinates.php +++ b/app/DataTransferObjects/MapCoordinates.php @@ -8,8 +8,9 @@ use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; +use Stringable; -class MapCoordinates +class MapCoordinates implements Stringable { public float $latitude = 44.4327; @@ -131,4 +132,9 @@ public function getZoom(): int { return $this->zoom; } + + public function __toString(): string + { + return \sprintf('@%s,%s,%sz', $this->latitude, $this->longitude, $this->zoom); + } } diff --git a/app/Http/Controllers/MapController.php b/app/Http/Controllers/MapController.php index 4b88fe7e..a49874b5 100644 --- a/app/Http/Controllers/MapController.php +++ b/app/Http/Controllers/MapController.php @@ -29,6 +29,7 @@ use Inertia\Inertia; use Inertia\LazyProp; use Inertia\Response; +use Laravel\Scout\Builder as ScoutBuilder; class MapController extends Controller { @@ -46,6 +47,9 @@ public function index(Request $request, MapCoordinates $coordinates): Response public function point(Point $point, MapCoordinates $coordinates): Response { + seo() + ->title($point->pointType->name); + return $this->render($coordinates, [ 'context' => 'point', 'report' => false, @@ -69,14 +73,6 @@ public function report(Point $point, MapCoordinates $coordinates): Response ]); } - public function material(Material $material, MapCoordinates $coordinates): Response - { - return $this->render($coordinates, [ - 'context' => 'material', - 'material' => $material, - ]); - } - public function suggest(Request $request, MapCoordinates $coordinates): JsonResource { $attributes = $request->validate([ @@ -139,6 +135,7 @@ public function search(Request $request, MapCoordinates $coordinates): Response { $attributes = $request->validate([ 'query' => ['required', 'string'], + 'material' => ['nullable', 'integer'], ]); return $this->render($coordinates, [ @@ -149,8 +146,11 @@ public function search(Request $request, MapCoordinates $coordinates): Response return []; } + $material = data_get($attributes, 'material'); + return SearchResultResource::collection( Point::search($attributes['query']) + ->when($material, fn (ScoutBuilder $query) => $query->where('material_ids', $material)) ->take(100) ->query( fn (Builder $query) => $query diff --git a/app/Http/Resources/SuggestionResource.php b/app/Http/Resources/SuggestionResource.php index 90878679..a0cd60dc 100644 --- a/app/Http/Resources/SuggestionResource.php +++ b/app/Http/Resources/SuggestionResource.php @@ -20,34 +20,38 @@ class SuggestionResource extends JsonResource public function toArray(Request $request): array { return match (\get_class($this->resource)) { - Point::class => $this->getPointArray(), - Material::class => $this->getMaterialArray(), - Location::class => $this->getLocationArray(), + Point::class => $this->getPointArray($request), + Material::class => $this->getMaterialArray($request), + Location::class => $this->getLocationArray($request), }; } - protected function getPointArray(): array + protected function getPointArray(Request $request): array { return [ 'name' => $this->business_name ?? $this->pointType->name, 'description' => $this->address, - 'url' => route('front.map.point', $this), 'type' => 'point', 'icon' => $this->serviceType->slug, + 'url' => $this->url, ]; } - protected function getMaterialArray(): array + protected function getMaterialArray(Request $request): array { return [ 'name' => $this->name, - 'url' => route('front.map.material', $this), 'type' => 'material', 'icon' => $this->icon, + 'url' => route('front.map.search', [ + 'coordinates' => $request->coordinates, + 'query' => $this->name, + 'material' => $this->id, + ]), ]; } - protected function getLocationArray(): array + protected function getLocationArray(Request $request): array { return [ 'name' => $this->name, diff --git a/app/Models/Point.php b/app/Models/Point.php index 61b25bd7..89b8617a 100644 --- a/app/Models/Point.php +++ b/app/Models/Point.php @@ -194,6 +194,11 @@ public function toSearchableArray(): array 'point_type_id' => (string) $this->pointType->id, 'address' => $this->address, 'point_type' => $this->pointType->name, + + 'material_ids' => $this->materials + ->pluck('id') + ->map(fn ($id) => (string) $id), + 'materials' => $this->materials ->pluck('name'), @@ -251,6 +256,10 @@ public static function getTypesenseModelSettings(): array 'name' => 'address', 'type' => 'string', ], + [ + 'name' => 'material_ids', + 'type' => 'string[]', + ], [ 'name' => 'materials', 'type' => 'string[]', diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 862e5953..5ea277f1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,8 +5,6 @@ namespace App\Providers; use App\Services\Filter; -use Filament\Facades\Filament; -use Filament\Navigation\NavigationGroup; use Filament\Notifications\Livewire\DatabaseNotifications; use Filament\Support\Assets\Css; use Filament\Support\Assets\Js; @@ -32,23 +30,7 @@ public function boot(): void Number::useLocale(app()->getLocale()); - Filament::serving(function () { - Filament::registerNavigationGroups([ - NavigationGroup::make() - ->label('Settings') - // ->icon('heroicon-s-cog') - ->collapsed(), - ]); - - Filament::registerNavigationItems([ - ]); - }); - - Filament::registerNavigationGroups([ - 'Harta' => NavigationGroup::make()->label(__('nav.harta')), - 'Rapoarte' => NavigationGroup::make()->label(__('nav.reports')), - 'Settings' => NavigationGroup::make()->label(__('nav.settings')), - ]); + $this->setSeoDefaults(); } /** @@ -99,6 +81,21 @@ protected function enforceMorphMap(): void ]); } + protected function setSeoDefaults(): void + { + seo() + ->withUrl() + ->title( + default: config('app.name'), + modifier: fn (string $title) => $title . ' — ' . config('app.name') + ) + // TODO: Add a default description + // ->description(default: '') + ->locale(app()->getLocale()) + ->favicon() + ->twitter(); + } + protected function registerStrMacros(): void { Str::macro('initials', fn (?string $value) => collect(explode(' ', (string) $value)) diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 44ede97c..f5315d9a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -36,13 +36,47 @@ public function boot(): void protected function configureRateLimiting(): void { - RateLimiter::for('register', function (Request $request) { - return Limit::perMinute(config('throttle.register_limit'))->by($request->ip()); - }); + RateLimiter::for( + 'register', + fn (Request $request) => Limit::perMinute(config('throttle.register')) + ->by($request->ip()) + ); - RateLimiter::for('login', function (Request $request) { - return Limit::perMinute(config('throttle.login_limit'))->by($request->ip()); - }); + RateLimiter::for( + 'login', + fn (Request $request) => Limit::perMinute(config('throttle.login')) + ->by($request->ip()) + ); + + RateLimiter::for( + 'forgot-password', + fn (Request $request) => Limit::perMinute(config('throttle.forgot-password')) + ->by($request->ip()) + ); + + RateLimiter::for( + 'reset-password', + fn (Request $request) => Limit::perMinute(config('throttle.reset-password')) + ->by($request->ip()) + ); + + RateLimiter::for( + 'submit', + fn (Request $request) => Limit::perMinute(config('throttle.submit')) + ->by($request->user()?->id ?: $request->ip()) + ); + + RateLimiter::for( + 'media', + fn (Request $request) => Limit::perMinute(config('throttle.media')) + ->by($request->user()?->id ?: $request->ip()) + ); + + RateLimiter::for( + 'map', + fn (Request $request) => Limit::perMinute(config('throttle.map')) + ->by($request->user()?->id ?: $request->ip()) + ); } public static function getDashboardUrl(): string diff --git a/app/Services/Filter.php b/app/Services/Filter.php index 9bcea3fe..6a532fac 100644 --- a/app/Services/Filter.php +++ b/app/Services/Filter.php @@ -41,7 +41,7 @@ public static function getValue(mixed $value): mixed return Str::of($value) ->explode(',') ->map(fn ($v) => self::getValue($v)) - ->all();; + ->all(); } if ($value === 'true') { diff --git a/composer.json b/composer.json index ed411086..6005f196 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "license": "MIT", "require": { "php": "^8.2", + "archtechx/laravel-seo": "^0.10.1", "dotswan/filament-map-picker": "^1.2", "filament/filament": "^3.2", "filament/forms": "^3.2", diff --git a/composer.lock b/composer.lock index ff38f3bc..f7473552 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "551c9f8a6326570289966555ade8a458", + "content-hash": "31b304429e71c03ab7416a5b92f92818", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -72,6 +72,64 @@ }, "time": "2024-09-16T12:59:37+00:00" }, + { + "name": "archtechx/laravel-seo", + "version": "v0.10.1", + "source": { + "type": "git", + "url": "https://github.com/archtechx/laravel-seo.git", + "reference": "76d0f9efac160e499daae2d1a79e2d09ca0daaae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/archtechx/laravel-seo/zipball/76d0f9efac160e499daae2d1a79e2d09ca0daaae", + "reference": "76d0f9efac160e499daae2d1a79e2d09ca0daaae", + "shasum": "" + }, + "require": { + "illuminate/support": "^10.0|^11.0", + "illuminate/view": "^10.0|^11.0", + "php": "^8.2" + }, + "require-dev": { + "intervention/image": "^2.7", + "nunomaduro/larastan": "^2.4", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "ArchTech\\SEO\\SEOServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "ArchTech\\SEO\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Štancl", + "email": "samuel@archte.ch" + } + ], + "support": { + "issues": "https://github.com/archtechx/laravel-seo/issues", + "source": "https://github.com/archtechx/laravel-seo/tree/v0.10.1" + }, + "time": "2024-05-03T22:09:30+00:00" + }, { "name": "aws/aws-crt-php", "version": "v1.2.6", diff --git a/config/throttle.php b/config/throttle.php index 975dc39e..d71ed09f 100644 --- a/config/throttle.php +++ b/config/throttle.php @@ -4,9 +4,25 @@ return [ - 'register_limit' => (int) env('THROTTLE_REGISTER_PER_MINUTE', 10), - 'login_limit' => (int) env('THROTTLE_LOGINS_PER_MINUTE', 5), - 'donation_limit' => (int) env('THROTTLE_DONATIONS_PER_MINUTE', 5), - 'volunteer_limit' => (int) env('THROTTLE_VOLUNTEERS_PER_MINUTE', 5), + // Number of registration attempts allowed per minute + 'register' => (int) env('THROTTLE_REGISTER_PER_MINUTE', 10), + + // Number of login attempts allowed per minute + 'login' => (int) env('THROTTLE_LOGIN_PER_MINUTE', 5), + + // Number of forgot-password attempts allowed per minute + 'forgot-password' => (int) env('THROTTLE_FORGOT_PASSWORD_PER_MINUTE', 5), + + // Number of reset-password attempts allowed per minute + 'reset-password' => (int) env('THROTTLE_RESET_PASSWORD_PER_MINUTE', 10), + + // Number of map interactions allowed per minute + 'map' => (int) env('THROTTLE_MAP_PER_MINUTE', 120), + + // Number of new point or report attempts allowed per minute + 'submit' => (int) env('THROTTLE_SUBMIT_PER_MINUTE', 30), + + // Number of media uploads or deletions allowed per minute + 'media' => (int) env('THROTTLE_MEDIA_PER_MINUTE', 30), ]; diff --git a/lang/ro.json b/lang/ro.json index fba897e4..caa919d0 100644 --- a/lang/ro.json +++ b/lang/ro.json @@ -30,6 +30,7 @@ "sidebar.clear_filters_label": "Șterge filtre", "sidebar.search": "Caută", "sidebar.results": "{0} 0 rezultate|{1} 1 rezultat|[2,19] :count rezultate|[20,*] de rezultate", + "sidebar.search_term": "Căutare după \":query\"", "sidebar.no_results_found": "Nu am găsit niciun rezultat pentru \":query\". în zona de căutare. Folosiți un alt termen de căutare sau extindeți zona prin zoom out pe hartă.", "sidebar.see_all_points": "Vezi toate punctele", diff --git a/public/robots.txt b/public/robots.txt index eb053628..f8a0a9ba 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,2 @@ User-agent: * -Disallow: +Disallow: /admin diff --git a/resources/js/Components/Accordion.vue b/resources/js/Components/Accordion.vue index ace229e3..6823d8b4 100644 --- a/resources/js/Components/Accordion.vue +++ b/resources/js/Components/Accordion.vue @@ -2,11 +2,15 @@ @@ -32,6 +43,10 @@ type: Boolean, default: true, }, + simple: { + type: Boolean, + default: false, + }, as: { type: [Object, String], default: 'div', diff --git a/resources/js/Components/Form/MaterialsChecklist.vue b/resources/js/Components/Form/MaterialsChecklist.vue index 203ccf41..7f576ee4 100644 --- a/resources/js/Components/Form/MaterialsChecklist.vue +++ b/resources/js/Components/Form/MaterialsChecklist.vue @@ -15,9 +15,17 @@ @keydown.enter.prevent /> -