first commit

This commit is contained in:
Doar Mihai
2026-04-28 16:10:27 +03:00
commit b73f568d90
107 changed files with 18067 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Resources\CityResource;
use App\Http\Resources\AttractionResource;
use App\Models\City;
use App\Jobs\FetchCityDataJob;
use App\Services\WikipediaService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class CityController extends Controller
{
/**
* Search for cities
*/
public function search(Request $request, WikipediaService $wikipediaService)
{
$validated = $request->validate([
'q' => 'required|string|min:2|max:255',
]);
$query = $validated['q'];
// Add to search history if session is available (for web routes)
if ($request->hasSession()) {
$this->addToSearchHistory($request, $query);
}
// Search in local database first
$localResults = $this->searchLocalCities($query);
$wikipediaResults = null;
// If no local results, fetch from Wikipedia and create new city
if ($localResults->isEmpty()) {
$newCity = $this->createCityFromWikipedia($query, $wikipediaService);
if ($newCity) {
$localResults = collect([$newCity]);
}
}
$response = [
'local' => CityResource::collection($localResults),
'wikipedia' => $wikipediaResults,
];
// Add recent searches if session is available
if ($request->hasSession()) {
$response['recent_searches'] = $request->session()->get('search_history', []);
}
return response()->json($response);
}
/**
* Add search query to session history
* Note: This method should only be called when hasSession() is true
*/
private function addToSearchHistory(Request $request, string $query): void
{
if (!$request->hasSession()) {
return;
}
$searchHistory = $request->session()->get('search_history', []);
// Remove duplicate if exists
$searchHistory = array_filter($searchHistory, function($item) use ($query) {
return strtolower($item) !== strtolower($query);
});
// Add new search to the beginning
array_unshift($searchHistory, $query);
// Keep only last 10 searches
$searchHistory = array_slice($searchHistory, 0, 10);
$request->session()->put('search_history', $searchHistory);
}
/**
* Search for cities in local database
*/
private function searchLocalCities(string $query)
{
return City::where('name', 'LIKE', "%{$query}%")
->orWhere('country', 'LIKE', "%{$query}%")
->limit(10)
->get();
}
/**
* Create a city from Wikipedia data
*/
private function createCityFromWikipedia(string $query, WikipediaService $wikipediaService)
{
try {
$wikipediaData = $wikipediaService->getCitySummary($query);
if (!$wikipediaData) {
return null;
}
$countryName = $this->fetchCountryName($wikipediaData['title']);
$city = City::create([
'name' => $wikipediaData['title'],
'country' => $countryName,
'latitude' => $wikipediaData['latitude'] ?? null,
'longitude' => $wikipediaData['longitude'] ?? null,
'wikipedia_summary' => $wikipediaData['summary'] ?? null,
'wikipedia_url' => $wikipediaData['url'] ?? null,
'thumbnail_url' => $wikipediaData['thumbnail'] ?? null,
'image_url' => $wikipediaData['image'] ?? null,
'wikipedia_page_id' => $wikipediaData['page_id'] ?? null,
'wikipedia_cached_at' => now(),
]);
// Dispatch job to fetch attractions for the newly created city
if ($city && $city->latitude && $city->longitude) {
\App\Jobs\FetchAttractionsJob::dispatch($city);
}
return $city;
} catch (\Exception $e) {
\Log::error('Failed to create city from Wikipedia', [
'query' => $query,
'error' => $e->getMessage(),
]);
return null;
}
}
/**
* Fetch country name from external API
*/
private function fetchCountryName(string $cityName)
{
try {
$response = Http::post('https://countriesnow.space/api/v0.1/countries/population/cities', [
'city' => $cityName,
]);
if ($response->successful()) {
$data = $response->json();
return $data['data']['country'] ?? 'Unknown';
}
return 'Unknown';
} catch (\Exception $e) {
\Log::error('Failed to fetch country name', [
'city' => $cityName,
'error' => $e->getMessage(),
]);
return 'Unknown';
}
}
/**
* Display the specified city
*/
public function show(City $city)
{
$city->loadCount('attractions');
// fetch attractions, or dispatch a job to fetch them
return new CityResource($city);
}
/**
* Get attractions for a city with pagination
*/
public function attractions(Request $request, City $city)
{
$validated = $request->validate([
'category' => 'nullable|string|max:50',
'limit' => 'nullable|integer|min:1|max:100',
'page' => 'nullable|integer|min:1',
]);
$query = $city->attractions()
->select(['id', 'city_id', 'name', 'description', 'latitude', 'longitude', 'category', 'rating', 'image_url'])
->orderBy('name', 'asc');
// Filter by category if provided
if ($request->has('category') && $request->category) {
$query->where('category', $request->category);
}
// Use pagination for better performance
$perPage = $request->input('limit', 20);
$attractions = $query->paginate($perPage);
return AttractionResource::collection($attractions);
}
/**
* Get map data for a city with optimized query
*/
public function mapData(City $city)
{
// Eager load only necessary columns for map display
$city->load(['attractions' => function ($query) {
$query->select(['id', 'city_id', 'name', 'latitude', 'longitude', 'category']);
}]);
return response()->json([
'city' => [
'name' => $city->name,
'latitude' => (float) $city->latitude,
'longitude' => (float) $city->longitude,
],
'attractions' => $city->attractions->map(function ($attraction) {
return [
'id' => $attraction->id,
'name' => $attraction->name,
'latitude' => (float) $attraction->latitude,
'longitude' => (float) $attraction->longitude,
'category' => $attraction->category,
];
}),
]);
}
/**
* Add a new city (Protected)
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'country' => 'required|string|max:255',
'latitude' => 'nullable|numeric|between:-90,90',
'longitude' => 'nullable|numeric|between:-180,180',
]);
$city = City::create($validated);
// Dispatch job to fetch data from Wikipedia
FetchCityDataJob::dispatch($city);
return new CityResource($city);
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Resources\CityResource;
use App\Models\City;
use Illuminate\Http\Request;
class FavoriteController extends Controller
{
/**
* Get user's favorite cities
*/
public function index(Request $request)
{
$favorites = $request->user()
->favoriteCities()
->withCount('attractions')
->get();
return CityResource::collection($favorites);
}
/**
* Add city to favorites
*/
public function store(Request $request, City $city)
{
$user = $request->user();
// Check if already favorited
if ($user->favoriteCities()->where('city_id', $city->id)->exists()) {
return response()->json([
'message' => 'City already in favorites',
], 409);
}
$user->favoriteCities()->attach($city->id);
return response()->json([
'message' => 'City added to favorites',
'city' => new CityResource($city),
], 201);
}
/**
* Remove city from favorites
*/
public function destroy(Request $request, City $city)
{
$user = $request->user();
$user->favoriteCities()->detach($city->id);
return response()->json([
'message' => 'City removed from favorites',
]);
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Resources\TripResource;
use App\Models\Trip;
use Illuminate\Http\Request;
class TripController extends Controller
{
/**
* Display a listing of user's trips
*/
public function index(Request $request)
{
$trips = $request->user()
->trips()
->withCount('cities')
->latest()
->get();
return TripResource::collection($trips);
}
/**
* Store a newly created trip
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'city_ids' => 'nullable|array',
'city_ids.*' => 'exists:cities,id',
]);
$trip = $request->user()->trips()->create([
'name' => $validated['name'],
'description' => $validated['description'] ?? null,
]);
// Attach cities if provided
if (!empty($validated['city_ids'])) {
$cityData = [];
foreach ($validated['city_ids'] as $index => $cityId) {
$cityData[$cityId] = ['order' => $index];
}
$trip->cities()->attach($cityData);
}
$trip->load('cities')->loadCount('cities');
return new TripResource($trip);
}
/**
* Display the specified trip
*/
public function show(Request $request, Trip $trip)
{
// Check if trip belongs to user
if ($trip->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$trip->load('cities')->loadCount('cities');
return new TripResource($trip);
}
/**
* Update the specified trip
*/
public function update(Request $request, Trip $trip)
{
// Check if trip belongs to user
if ($trip->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$validated = $request->validate([
'name' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'city_ids' => 'nullable|array',
'city_ids.*' => 'exists:cities,id',
]);
$trip->update([
'name' => $validated['name'] ?? $trip->name,
'description' => $validated['description'] ?? $trip->description,
]);
// Update cities if provided
if (isset($validated['city_ids'])) {
$cityData = [];
foreach ($validated['city_ids'] as $index => $cityId) {
$cityData[$cityId] = ['order' => $index];
}
$trip->cities()->sync($cityData);
}
$trip->load('cities')->loadCount('cities');
return new TripResource($trip);
}
/**
* Remove the specified trip
*/
public function destroy(Request $request, Trip $trip)
{
// Check if trip belongs to user
if ($trip->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$trip->delete();
return response()->json([
'message' => 'Trip deleted successfully',
]);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display the authenticated user's profile
*/
public function show(Request $request)
{
return new UserResource($request->user());
}
/**
* Update the authenticated user's profile
*/
public function update(Request $request)
{
$validated = $request->validate([
'name' => 'sometimes|required|string|max:255',
'email' => 'sometimes|required|email|unique:users,email,' . $request->user()->id,
]);
$request->user()->update($validated);
return new UserResource($request->user());
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
/**
* Show the login form
*/
public function showLoginForm()
{
return view('auth.login');
}
/**
* Handle login request
*/
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials, $request->filled('remember'))) {
$request->session()->regenerate();
return redirect()->intended('/');
}
throw ValidationException::withMessages([
'email' => 'The provided credentials do not match our records.',
]);
}
/**
* Handle logout request
*/
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class RegisterController extends Controller
{
/**
* Show the registration form
*/
public function showRegistrationForm()
{
return view('auth.register');
}
/**
* Handle registration request
*/
public function register(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
Auth::login($user);
return redirect('/');
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Register a new user
*/
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
], 201);
}
/**
* Login user
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
]);
}
/**
* Logout user (Revoke token)
*/
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Successfully logged out',
]);
}
/**
* Get authenticated user
*/
public function user(Request $request)
{
return response()->json($request->user());
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use App\Models\City;
use Illuminate\Http\Request;
class CityController extends Controller
{
/**
* Display the city details page
*/
public function show(City $city)
{
$city->load('attractions');
return view('cities.show', compact('city'));
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}