diff --git a/.env.example b/.env.example index a843a46f..c9162340 100644 --- a/.env.example +++ b/.env.example @@ -62,3 +62,5 @@ GOOGLE_REDIRECT_URI=http://escooters.blumilk.localhost/login/google/redirect FACEBOOK_CLIENT_ID= FACEBOOK_CLIENT_SECRET= FACEBOOK_REDIRECT_URI=http://escooters.blumilk.localhost/login/facebook/redirect + +OPENAI_API_KEY= diff --git a/app/Exceptions/ExceptionHandler.php b/app/Exceptions/ExceptionHandler.php index f029365c..ae4f1735 100644 --- a/app/Exceptions/ExceptionHandler.php +++ b/app/Exceptions/ExceptionHandler.php @@ -40,6 +40,10 @@ public function render($request, Throwable $exception) return back()->withErrors($exception->errors()); } + if ($exception instanceof OpenAiException) { + return response()->json(["error" => $exception->getMessage()], $exception->getCode()); + } + $response = parent::render($request, $exception); $statusCode = $response->status(); diff --git a/app/Exceptions/OpenAiException.php b/app/Exceptions/OpenAiException.php new file mode 100644 index 00000000..cc20998b --- /dev/null +++ b/app/Exceptions/OpenAiException.php @@ -0,0 +1,14 @@ +where("city_id", $city->id) + ->first(); + + if (!$rules || $rules->rules_en === null || $rules->rules_pl === null) { + $cityData = [ + "cityId" => $city->id, + "countryId" => $country->id, + "cityName" => $city->name, + "countryName" => $country->name, + ]; + $importer = new OpenAIService(); + $data = $importer->importRulesForCity($cityData, true); + } else { + $data = [ + "country" => $country->name, + "city" => $city->name, + "rules_en" => $rules->rules_en, + "rules_pl" => $rules->rules_pl, + ]; + } + + return response()->json($data); + } + + public function importRules(Request $request, OpenAIService $importer): JsonResponse + { + $force = $request->input("force", false); + $importer->importRulesForAllCities($force); + + return response()->json(["message" => "Rules import started"]); + } +} diff --git a/app/Http/Controllers/RulesController.php b/app/Http/Controllers/RulesController.php new file mode 100644 index 00000000..c3b822d5 --- /dev/null +++ b/app/Http/Controllers/RulesController.php @@ -0,0 +1,47 @@ +where("city_id", $city->id) + ->first(); + + if (!$rules || $rules->rules_en === null || $rules->rules_pl === null) { + $cityData = [ + "cityId" => $city->id, + "countryId" => $country->id, + "cityName" => $city->name, + "countryName" => $country->name, + ]; + $importer = new OpenAIService(); + $data = $importer->importRulesForCity($cityData, true); + } else { + $data = [ + "country" => $country->name, + "city" => $city->name, + "rules_en" => $rules->rules_en, + "rules_pl" => $rules->rules_pl, + ]; + } + + return $data; + } + + public function importRules(Request $request, OpenAIService $importer): void + { + $force = $request->input("force", false); + $importer->importRulesForAllCities($force); + } +} diff --git a/app/Jobs/ImportCityRulesJob.php b/app/Jobs/ImportCityRulesJob.php new file mode 100644 index 00000000..6c6458e8 --- /dev/null +++ b/app/Jobs/ImportCityRulesJob.php @@ -0,0 +1,30 @@ +importRulesForCity($this->cityData, $this->force); + } +} diff --git a/app/Models/Rules.php b/app/Models/Rules.php new file mode 100644 index 00000000..60170c95 --- /dev/null +++ b/app/Models/Rules.php @@ -0,0 +1,29 @@ +belongsTo(City::class); + } + + public function country(): BelongsTo + { + return $this->belongsTo(Country::class); + } +} diff --git a/app/Services/OpenAIService.php b/app/Services/OpenAIService.php new file mode 100644 index 00000000..41765778 --- /dev/null +++ b/app/Services/OpenAIService.php @@ -0,0 +1,130 @@ +client = OpenAI::client(config("openai.token")); + } catch (Throwable $e) { + throw new OpenAiException(); + } + + $this->countriesKnownToHaveUniformRules = Storage::disk("public")->json("citiesKnownToHaveUniformRules.json"); + } + + public function importRulesForAllCities(bool $force): void + { + $cities = City::query()->whereHas("cityProviders")->orderBy("country_id")->get(); + + $importInfo = ImportInfo::query()->create([ + "who_runs_it" => "admin", + "status" => "running", + ]); + $jobs = []; + + foreach ($cities as $city) { + $cityData = [ + "cityId" => $city->id, + "countryId" => $city->country_id, + "cityName" => $city->name, + "countryName" => $city->country->name, + ]; + $jobs[] = new ImportCityRulesJob($cityData, $force); + ImportCityRulesJob::dispatch($cityData, $force); + } + + Bus::batch($jobs) + ->catch(fn() => ImportInfoDetail::query()->updateOrCreate([ + "import_info_id" => $importInfo->id, + "provider_name" => "OpenAI", + "code" => 400, + ])) + ->finally(fn() => ImportInfo::query()->where("id", $importInfo->id)->update([ + "status" => "finished", + ])) + ->dispatch(); + } + + public function importRulesForCity(array $cityData, bool $force): array + { + $cityId = $cityData["cityId"]; + $countryId = $cityData["countryId"]; + $cityName = $cityData["cityName"]; + $countryName = $cityData["countryName"]; + + $promptEn = "Act as a helpful assistant. Explain what are the legal limitations for riding electric scooters in $cityName, $countryName? Contain information about: max speed, helmet requirements, allowed ABV, passengers, other relevant details. Be formal, speak English. Don't include city name in your response. If you don't have information answering the question, write 'null'."; + $promptPl = "Translate to polish: "; + $currentRulesInCountry = Rules::query()->where("country_id", $countryId)->first(); + + if ($this->checkIfRulesExist($countryName, $currentRulesInCountry) && !$force) { + $rulesEn = $currentRulesInCountry->rules_en; + $rulesPl = $currentRulesInCountry->rules_pl; + } else { + $rulesEn = $this->askGPT($promptEn); + $rulesPl = $this->askGPT($promptPl . $rulesEn); + } + + if (strlen($rulesEn) < 700 || strlen($rulesPl) < 700) { + return [ + "city" => $cityName, + "country" => $countryName, + "rules_en" => null, + "rules_pl" => null, + ]; + } + + Rules::query()->updateOrCreate([ + "city_id" => $cityId, + "country_id" => $countryId, + "rules_en" => $rulesEn, + "rules_pl" => $rulesPl, + ]); + + return [ + "city" => $cityName, + "country" => $countryName, + "rules_en" => $rulesEn, + "rules_pl" => $rulesPl, + ]; + } + + private function askGPT(string $prompt): string + { + $response = $this->client->chat()->create([ + "model" => "gpt-3.5-turbo", + "messages" => [ + [ + "role" => "user", + "content" => $prompt, + ], + ], + ]); + + return $response["choices"][0]["message"]["content"]; + } + + private function checkIfRulesExist(string $countryName, ?Rules $currentRulesInCountry): bool + { + return in_array($countryName, $this->countriesKnownToHaveUniformRules, true) && $currentRulesInCountry !== null && $currentRulesInCountry->rules_en !== null && $currentRulesInCountry->rules_pl !== null; + } +} diff --git a/composer.json b/composer.json index 7d390c51..1bd1e16e 100644 --- a/composer.json +++ b/composer.json @@ -10,13 +10,15 @@ "ext-gd": "*", "ext-pdo": "*", "guzzlehttp/guzzle": "^7.7", - "inertiajs/inertia-laravel": "^0.6.9", + "dedoc/scramble": "^0.9.0", + "inertiajs/inertia-laravel": "^1.0.0", "intervention/image": "2.*", "laravel/framework": "^10.13.0", "laravel/sanctum": "^3.2.5", "laravel/socialite": "^5.10", "laravel/tinker": "^2.8.1", + "openai-php/client": "^0.8.1", "sentry/sentry-laravel": "^3.7", "spatie/laravel-permission": "^6.1", "stichoza/google-translate-php": "^5.1", diff --git a/composer.lock b/composer.lock index 28c5e7f0..dd92c706 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": "b855f256a634ad0b33f1370b2d34f81d", + "content-hash": "4cc2044218af93556d4655e969efa960", "packages": [ { "name": "brick/math", @@ -201,6 +201,80 @@ ], "time": "2023-12-20T15:40:13+00:00" }, + { + "name": "dedoc/scramble", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/dedoc/scramble.git", + "reference": "6280da6809eecaa03243d726b957cc174b1ccb70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/6280da6809eecaa03243d726b957cc174b1ccb70", + "reference": "6280da6809eecaa03243d726b957cc174b1ccb70", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0", + "nikic/php-parser": "^5.0", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.0", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "laravel/pint": "^v1.1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.34", + "pestphp/pest-plugin-laravel": "^2.3", + "phpunit/phpunit": "^10.5", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Dedoc\\Scramble\\ScrambleServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Dedoc\\Scramble\\": "src", + "Dedoc\\Scramble\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Lytvynenko", + "email": "litvinenko95@gmail.com", + "role": "Developer" + } + ], + "description": "Automatic generation of API documentation for Laravel applications.", + "homepage": "https://github.com/dedoc/scramble", + "keywords": [ + "documentation", + "laravel", + "openapi" + ], + "support": { + "issues": "https://github.com/dedoc/scramble/issues", + "source": "https://github.com/dedoc/scramble/tree/v0.9.0" + }, + "funding": [ + { + "url": "https://github.com/romalytvynenko", + "type": "github" + } + ], + "time": "2024-03-11T19:27:28+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -1239,27 +1313,27 @@ }, { "name": "inertiajs/inertia-laravel", - "version": "v0.6.11", + "version": "v1.0.0", "source": { "type": "git", "url": "https://github.com/inertiajs/inertia-laravel.git", - "reference": "2a1e19048f95c0e4adb2b2733f9119e49c4fc09f" + "reference": "fcf3d6db1a259a55d8d18cf43fc971202c1f6b0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/2a1e19048f95c0e4adb2b2733f9119e49c4fc09f", - "reference": "2a1e19048f95c0e4adb2b2733f9119e49c4fc09f", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/fcf3d6db1a259a55d8d18cf43fc971202c1f6b0d", + "reference": "fcf3d6db1a259a55d8d18cf43fc971202c1f6b0d", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^6.0|^7.0|^8.74|^9.0|^10.0", - "php": "^7.2|~8.0.0|~8.1.0|~8.2.0|~8.3.0" + "laravel/framework": "^8.74|^9.0|^10.0|^11.0", + "php": "^7.3|~8.0.0|~8.1.0|~8.2.0|~8.3.0" }, "require-dev": { "mockery/mockery": "^1.3.3", - "orchestra/testbench": "^4.0|^5.0|^6.4|^7.0|^8.0", - "phpunit/phpunit": "^8.0|^9.5.8", + "orchestra/testbench": "^6.4|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^8.0|^9.5.8|^10.4", "roave/security-advisories": "dev-master" }, "suggest": { @@ -1271,6 +1345,9 @@ "providers": [ "Inertia\\ServiceProvider" ] + }, + "branch-alias": { + "dev-master": "1.x-dev" } }, "autoload": { @@ -1299,7 +1376,7 @@ ], "support": { "issues": "https://github.com/inertiajs/inertia-laravel/issues", - "source": "https://github.com/inertiajs/inertia-laravel/tree/v0.6.11" + "source": "https://github.com/inertiajs/inertia-laravel/tree/v1.0.0" }, "funding": [ { @@ -1307,7 +1384,7 @@ "type": "github" } ], - "time": "2023-10-27T10:59:02+00:00" + "time": "2024-03-09T00:30:58+00:00" }, { "name": "intervention/image", @@ -1845,16 +1922,16 @@ }, { "name": "laravel/socialite", - "version": "v5.13.2", + "version": "v5.14.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "278d4615f68205722b3a129135774b3764b28a90" + "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/278d4615f68205722b3a129135774b3764b28a90", - "reference": "278d4615f68205722b3a129135774b3764b28a90", + "url": "https://api.github.com/repos/laravel/socialite/zipball/c7b0193a3753a29aff8ce80aa2f511917e6ed68a", + "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a", "shasum": "" }, "require": { @@ -1913,7 +1990,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2024-04-26T13:48:16+00:00" + "time": "2024-05-03T20:31:38+00:00" }, { "name": "laravel/tinker", @@ -3095,6 +3172,98 @@ ], "time": "2023-11-13T09:31:12+00:00" }, + { + "name": "openai-php/client", + "version": "v0.8.5", + "source": { + "type": "git", + "url": "https://github.com/openai-php/client.git", + "reference": "0f755fafa4d3f8d5c8ed964d3166d078fac0605a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openai-php/client/zipball/0f755fafa4d3f8d5c8ed964d3166d078fac0605a", + "reference": "0f755fafa4d3f8d5c8ed964d3166d078fac0605a", + "shasum": "" + }, + "require": { + "php": "^8.1.0", + "php-http/discovery": "^1.19.4", + "php-http/multipart-stream-builder": "^1.3.0", + "psr/http-client": "^1.0.3", + "psr/http-client-implementation": "^1.0.1", + "psr/http-factory-implementation": "*", + "psr/http-message": "^1.1.0|^2.0.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.8.1", + "guzzlehttp/psr7": "^2.6.2", + "laravel/pint": "^1.15.0", + "mockery/mockery": "^1.6.11", + "nunomaduro/collision": "^7.10.0", + "pestphp/pest": "^2.34.6", + "pestphp/pest-plugin-arch": "^2.7", + "pestphp/pest-plugin-type-coverage": "^2.8.1", + "phpstan/phpstan": "^1.10.66", + "rector/rector": "^1.0.4", + "symfony/var-dumper": "^6.4.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/OpenAI.php" + ], + "psr-4": { + "OpenAI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri" + } + ], + "description": "OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API", + "keywords": [ + "GPT-3", + "api", + "client", + "codex", + "dall-e", + "language", + "natural", + "openai", + "php", + "processing", + "sdk" + ], + "support": { + "issues": "https://github.com/openai-php/client/issues", + "source": "https://github.com/openai-php/client/tree/v0.8.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2024-04-15T19:11:23+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v2.6.3", @@ -3541,6 +3710,62 @@ "abandoned": "psr/http-factory", "time": "2023-04-14T14:16:17+00:00" }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0" + }, + "time": "2023-04-28T14:10:22+00:00" + }, { "name": "php-http/promise", "version": "1.3.1", @@ -3778,6 +4003,53 @@ ], "time": "2024-03-03T02:14:58+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.29.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", + "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" + }, + "time": "2024-05-06T12:04:23+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -4743,6 +5015,66 @@ ], "time": "2023-10-12T14:38:46+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.16.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0", + "pestphp/pest": "^1.22", + "phpunit/phpunit": "^9.5.24", + "spatie/pest-plugin-test-time": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-03-20T07:29:11+00:00" + }, { "name": "spatie/laravel-permission", "version": "6.7.0", @@ -8006,16 +8338,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -8052,7 +8384,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -8068,7 +8400,7 @@ "type": "tidelift" } ], - "time": "2024-03-26T18:29:49+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "fakerphp/faker", @@ -8206,16 +8538,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.54.0", + "version": "v3.56.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08" + "reference": "4429303e62a4ce583ddfe64ff5c34c76bcf74931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2aecbc8640d7906c38777b3dcab6f4ca79004d08", - "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4429303e62a4ce583ddfe64ff5c34c76bcf74931", + "reference": "4429303e62a4ce583ddfe64ff5c34c76bcf74931", "shasum": "" }, "require": { @@ -8287,7 +8619,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.54.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.56.0" }, "funding": [ { @@ -8295,7 +8627,7 @@ "type": "github" } ], - "time": "2024-04-17T08:12:13+00:00" + "time": "2024-05-07T15:50:05+00:00" }, { "name": "hamcrest/hamcrest-php", diff --git a/config/mapbox.php b/config/mapbox.php index 18a50d08..24b24359 100644 --- a/config/mapbox.php +++ b/config/mapbox.php @@ -4,6 +4,5 @@ return [ "token" => env("MAPBOX_TOKEN"), - "api_url" => env("MAPBOX_URL"), ]; diff --git a/config/openai.php b/config/openai.php new file mode 100644 index 00000000..aee31f8b --- /dev/null +++ b/config/openai.php @@ -0,0 +1,7 @@ + env("OPENAI_API_KEY"), +]; diff --git a/database/migrations/2024_01_07_182205_create_rules_for_cities_table.php b/database/migrations/2024_01_07_182205_create_rules_for_cities_table.php new file mode 100644 index 00000000..90232907 --- /dev/null +++ b/database/migrations/2024_01_07_182205_create_rules_for_cities_table.php @@ -0,0 +1,34 @@ +id(); + $table->timestamps(); + $table->unsignedBigInteger("city_id"); + $table->foreign("city_id") + ->references("id") + ->on("cities") + ->onDelete("cascade"); + $table->unsignedBigInteger("country_id"); + $table->foreign("country_id") + ->references("id") + ->on("countries") + ->onDelete("cascade"); + $table->text("rules_en")->nullable(); + $table->text("rules_pl")->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists("rules_for_cities"); + } +}; diff --git a/database/seeders/ProviderSeeder.php b/database/seeders/ProviderSeeder.php index de08d7a5..a30efa2f 100644 --- a/database/seeders/ProviderSeeder.php +++ b/database/seeders/ProviderSeeder.php @@ -61,6 +61,7 @@ public function run(): void ["name" => VeoDataImporter::getProviderName(), "color" => "#000000", "url" => "https://www.veoride.com/", "android_url" => "https://play.google.com/store/apps/details?id=com.pgt.veoride", "ios_url" => "https://apps.apple.com/us/app/veoride/id1279820696"], ["name" => WindDataImporter::getProviderName(), "color" => "#fffa00", "url" => "https://www.wind.co/", "android_url" => "https://play.google.com/store/apps/details?id=com.zen.zbike&referrer=utm_source%3Dofficialweb%26utm_medium%3Dother%26utm_term%3Dglobal", "ios_url" => "https://apps.apple.com/cn/app/yango-wind-e-scooter-sharing/id1247826304?utm_source=officialweb&utm_medium=other&time=1703025387273&utm_term=global"], ["name" => WheeMoveDataImporter::getProviderName(), "color" => "#31682d", "url" => "https://www.wheemove.com/", "android_url" => "https://play.google.com/store/apps/details?id=com.whee.android", "ios_url" => "https://apps.apple.com/us/app/whee-e-scooter-sharing/id1465111214?l=es&ls=1"], + ["name" => "OpenAI", "color" => "#ffffff", "url" => "https://openai.com"], ]; foreach ($providers as $provider) { diff --git a/lang/pl.json b/lang/pl.json index 644185ed..ee232706 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -168,6 +168,13 @@ "Login failed.": "Logowanie nie powiodło się.", "Please, rate that city": "Oceń miasto", "You can also login by:": "Możesz również zalogować się przez:", + "Rules import started.": "Import zasad uruchomiony.", + "Run rules import": "Uruchom import zasad", + "Found": "Znaleziono", + "loading info about rules, please wait": "ładowanie informacji o zasadach, proszę czekać", + "There was an error fetching rules.": "Wystąpił błąd podczas pobierania zasad.", + "Results found": "Znaleziono wyników", + "City not found.": "Miasto nieznalezione.", "Change in favorite city": "Zmiana w ulubionym mieście", "Hello!": "Witaj!", "There has been a change in your favorite city.": "Nastąpiła zmiana w Twoim ulubionym mieście.", diff --git a/public/providers/openai.png b/public/providers/openai.png new file mode 100644 index 00000000..18310efb Binary files /dev/null and b/public/providers/openai.png differ diff --git a/resources/css/app.css b/resources/css/app.css index 9430c6ad..2cb25885 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -25,6 +25,25 @@ .scrollbar::-webkit-scrollbar-thumb:hover { background: #d3d3d3; } + + div.regulations>div:nth-child(2) { + max-height: 0vh; + margin-bottom: 0px; + transition: all 0.5s ease-in-out; + } + + div.regulations>div:nth-child(2).show { + max-height: 50vh; + margin-bottom: 12px; + } + + div.regulations>div>svg { + transform: rotate(0deg); + } + + div.regulations>div>svg.rotated { + transform: rotate(-180deg); + } .provider-buttons { position: absolute; transition: all 0.15s ease-in-out; @@ -77,5 +96,6 @@ } .provider-buttons>a{ transition: all 0.15s ease-in-out; + } } diff --git a/resources/js/Pages/City/Index.vue b/resources/js/Pages/City/Index.vue index 0bd59b1a..69e51bfa 100644 --- a/resources/js/Pages/City/Index.vue +++ b/resources/js/Pages/City/Index.vue @@ -2,8 +2,8 @@ import Nav from '@/Shared/Layout/Nav.vue' import Map from '@/Shared/Layout/Map.vue' import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' -import { computed, onUnmounted, ref } from 'vue' -import { MapIcon, XMarkIcon, StarIcon, PaperAirplaneIcon } from '@heroicons/vue/24/outline' +import { computed, onUnmounted, ref, reactive } from 'vue' +import { MapIcon, XMarkIcon, StarIcon, PaperAirplaneIcon, ArrowDownIcon } from '@heroicons/vue/24/outline' import { useFilterStore } from '@/Shared/Stores/FilterStore' import FavoriteButton from '@/Shared/Components/FavoriteButton.vue' import ProviderIcons from '@/Shared/Components/ProviderIcons.vue' @@ -14,16 +14,24 @@ import { useToast } from 'vue-toastification' import Pagination from '@/Shared/Components/Pagination.vue' import InfoPopup from '@/Shared/Components/InfoPopup.vue' import Opinion from '@/Shared/Components/Opinion.vue' +import axios from 'axios' + const toast = useToast() const page = usePage() const isAuth = computed(() => page.props.auth.isAuth) +const regulationsOpen = ref(false) +const rules = reactive({ pl: 'ładowanie informacji o zasadach, proszę czekać...', en: 'loading info about rules, please wait...' }) const props = defineProps({ city: Object, providers: Object, cityOpinions: Object, }) +fetchRegulations() + +const currentLocale = ref(computed(() => page.props.locale)) +const currentRules = ref(computed(()=>rules[currentLocale.value])) const breakpoints = useBreakpoints(breakpointsTailwind) const isMobile = ref(breakpoints.smaller('lg')) @@ -57,6 +65,22 @@ function setRating(starIndex) { opinionForm.rating = starIndex } +function toggleRegulations() { + regulationsOpen.value = !regulationsOpen.value + +} + +function fetchRegulations() { + axios.get(`/api/rules/${props.city.country.name.toLowerCase()}/${props.city.name.toLowerCase()}`) + .then(response => { + rules.pl= response.data.rules_pl + rules.en = response.data.rules_en + }) + .catch(() => { + toast.error(__('There was an error fetching rules.')) + }) +} + const emptyRatingError = ref('') function createOpinion() { @@ -75,6 +99,7 @@ function createOpinion() { }, }) } + } @@ -106,7 +131,14 @@ function createOpinion() { {{ city.latitude }}, {{ city.longitude }} - +
+
+ {{ __('Rules') }} +
+
+
+            
+

{{ __('Add opinion') }} diff --git a/resources/js/Pages/Importers/Index.vue b/resources/js/Pages/Importers/Index.vue index b3592841..090aa92e 100644 --- a/resources/js/Pages/Importers/Index.vue +++ b/resources/js/Pages/Importers/Index.vue @@ -20,6 +20,11 @@ function runImporters() { router.post('/run-importers', []) toast.success(__('Importers started.')) } + +function runRules() { + router.post('/import-rules/', { force: false }) + toast.success(__('Rules import started.')) +}