Compare commits
9 Commits
b1aa29e877
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
aaca102b32
|
|||
|
e003cb2fb1
|
|||
|
c0645c1be4
|
|||
|
4616582cd1
|
|||
|
40a7c041d1
|
|||
|
3bf399eb97
|
|||
|
2de2195223
|
|||
|
458b9afa6b
|
|||
|
442529261b
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ yarn-error.log
|
|||||||
.session_id
|
.session_id
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
|
/.claude
|
||||||
/.phpunit.cache
|
/.phpunit.cache
|
||||||
/bootstrap/ssr
|
/bootstrap/ssr
|
||||||
/node_modules
|
/node_modules
|
||||||
|
|||||||
2
ai/.gitignore
vendored
Normal file
2
ai/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
134
app/Http/Controllers/Api/WeatherController.php
Normal file
134
app/Http/Controllers/Api/WeatherController.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\WeatherReport;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class WeatherController extends Controller
|
||||||
|
{
|
||||||
|
public function index(string $period = 'now'): JsonResponse
|
||||||
|
{
|
||||||
|
$period = in_array($period, ['now', 'daily', 'weekly']) ? $period : 'now';
|
||||||
|
|
||||||
|
return match ($period) {
|
||||||
|
'now' => $this->getCurrentConditions(),
|
||||||
|
'daily' => $this->getDailyForecast(),
|
||||||
|
'weekly' => $this->getWeeklyForecast(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentConditions(): JsonResponse
|
||||||
|
{
|
||||||
|
$latestHourly = WeatherReport::query()
|
||||||
|
->where('type', 'hourly')
|
||||||
|
->with('periods')
|
||||||
|
->orderByDesc('reported_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$currentPeriod = $latestHourly?->periods->first();
|
||||||
|
|
||||||
|
if (! $currentPeriod) {
|
||||||
|
return response()->json(['error' => 'No current conditions available'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'period' => 'now',
|
||||||
|
'reportedAt' => $latestHourly->reported_at->toIso8601String(),
|
||||||
|
'location' => [
|
||||||
|
'latitude' => $latestHourly->latitude,
|
||||||
|
'longitude' => $latestHourly->longitude,
|
||||||
|
],
|
||||||
|
'conditions' => [
|
||||||
|
'temperature' => $currentPeriod->temperature,
|
||||||
|
'temperatureUnit' => $currentPeriod->temperature_unit,
|
||||||
|
'shortForecast' => $currentPeriod->short_forecast,
|
||||||
|
'detailedForecast' => $currentPeriod->detailed_forecast,
|
||||||
|
'windSpeed' => $currentPeriod->wind_speed,
|
||||||
|
'windDirection' => $currentPeriod->wind_direction,
|
||||||
|
'humidity' => $currentPeriod->relative_humidity,
|
||||||
|
'dewpointCelsius' => $currentPeriod->dewpoint_celsius,
|
||||||
|
'precipitationProbability' => $currentPeriod->precipitation_probability,
|
||||||
|
'isDaytime' => $currentPeriod->is_daytime,
|
||||||
|
'startTime' => $currentPeriod->start_time->toIso8601String(),
|
||||||
|
'endTime' => $currentPeriod->end_time->toIso8601String(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDailyForecast(): JsonResponse
|
||||||
|
{
|
||||||
|
$latestHourly = WeatherReport::query()
|
||||||
|
->where('type', 'hourly')
|
||||||
|
->with('periods')
|
||||||
|
->orderByDesc('reported_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $latestHourly || $latestHourly->periods->isEmpty()) {
|
||||||
|
return response()->json(['error' => 'No daily forecast available'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$periods = $latestHourly->periods->map(fn ($period) => [
|
||||||
|
'periodNumber' => $period->period_number,
|
||||||
|
'startTime' => $period->start_time->toIso8601String(),
|
||||||
|
'endTime' => $period->end_time->toIso8601String(),
|
||||||
|
'isDaytime' => $period->is_daytime,
|
||||||
|
'temperature' => $period->temperature,
|
||||||
|
'temperatureUnit' => $period->temperature_unit,
|
||||||
|
'shortForecast' => $period->short_forecast,
|
||||||
|
'windSpeed' => $period->wind_speed,
|
||||||
|
'windDirection' => $period->wind_direction,
|
||||||
|
'humidity' => $period->relative_humidity,
|
||||||
|
'precipitationProbability' => $period->precipitation_probability,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'period' => 'daily',
|
||||||
|
'reportedAt' => $latestHourly->reported_at->toIso8601String(),
|
||||||
|
'location' => [
|
||||||
|
'latitude' => $latestHourly->latitude,
|
||||||
|
'longitude' => $latestHourly->longitude,
|
||||||
|
],
|
||||||
|
'periods' => $periods,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getWeeklyForecast(): JsonResponse
|
||||||
|
{
|
||||||
|
$latestWeekly = WeatherReport::query()
|
||||||
|
->where('type', 'weekly')
|
||||||
|
->with('periods')
|
||||||
|
->orderByDesc('reported_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $latestWeekly || $latestWeekly->periods->isEmpty()) {
|
||||||
|
return response()->json(['error' => 'No weekly forecast available'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$periods = $latestWeekly->periods->map(fn ($period) => [
|
||||||
|
'periodNumber' => $period->period_number,
|
||||||
|
'name' => $period->name,
|
||||||
|
'startTime' => $period->start_time->toIso8601String(),
|
||||||
|
'endTime' => $period->end_time->toIso8601String(),
|
||||||
|
'isDaytime' => $period->is_daytime,
|
||||||
|
'temperature' => $period->temperature,
|
||||||
|
'temperatureUnit' => $period->temperature_unit,
|
||||||
|
'shortForecast' => $period->short_forecast,
|
||||||
|
'detailedForecast' => $period->detailed_forecast,
|
||||||
|
'windSpeed' => $period->wind_speed,
|
||||||
|
'windDirection' => $period->wind_direction,
|
||||||
|
'precipitationProbability' => $period->precipitation_probability,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'period' => 'weekly',
|
||||||
|
'reportedAt' => $latestWeekly->reported_at->toIso8601String(),
|
||||||
|
'location' => [
|
||||||
|
'latitude' => $latestWeekly->latitude,
|
||||||
|
'longitude' => $latestWeekly->longitude,
|
||||||
|
],
|
||||||
|
'periods' => $periods,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,10 +53,7 @@ class WeatherController extends Controller
|
|||||||
'detailedForecast' => $period->detailed_forecast,
|
'detailedForecast' => $period->detailed_forecast,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$background = $this->getBackgroundForForecast(
|
$background = $this->getBackgroundForForecast($currentPeriod?->short_forecast);
|
||||||
$currentPeriod?->short_forecast,
|
|
||||||
$currentPeriod?->is_daytime ?? true
|
|
||||||
);
|
|
||||||
|
|
||||||
return Inertia::render('Weather', [
|
return Inertia::render('Weather', [
|
||||||
'current' => $currentPeriod ? [
|
'current' => $currentPeriod ? [
|
||||||
@@ -87,10 +84,10 @@ class WeatherController extends Controller
|
|||||||
/**
|
/**
|
||||||
* @return array{imageUrl: string|null, licenseHtml: string|null}
|
* @return array{imageUrl: string|null, licenseHtml: string|null}
|
||||||
*/
|
*/
|
||||||
private function getBackgroundForForecast(?string $forecast, bool $isDaytime): array
|
private function getBackgroundForForecast(?string $forecast): array
|
||||||
{
|
{
|
||||||
$folder = $this->mapForecastToFolder($forecast);
|
$folder = $this->mapForecastToFolder($forecast);
|
||||||
$timeOfDay = $isDaytime ? 'day' : 'night';
|
$timeOfDay = $this->isCurrentlyDaytime() ? 'day' : 'night';
|
||||||
$basePath = storage_path('app/public/backgrounds/'.$folder);
|
$basePath = storage_path('app/public/backgrounds/'.$folder);
|
||||||
|
|
||||||
if (! File::isDirectory($basePath)) {
|
if (! File::isDirectory($basePath)) {
|
||||||
@@ -153,6 +150,16 @@ class WeatherController extends Controller
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isCurrentlyDaytime(): bool
|
||||||
|
{
|
||||||
|
$sunriseHour = 6.75; // 6:45 AM
|
||||||
|
$sunsetHour = 17.5; // 5:30 PM
|
||||||
|
|
||||||
|
$currentHour = now()->hour + (now()->minute / 60);
|
||||||
|
|
||||||
|
return $currentHour >= $sunriseHour && $currentHour < $sunsetHour;
|
||||||
|
}
|
||||||
|
|
||||||
private function mapIconToEmoji(?string $forecast): string
|
private function mapIconToEmoji(?string $forecast): string
|
||||||
{
|
{
|
||||||
if (! $forecast) {
|
if (! $forecast) {
|
||||||
|
|||||||
@@ -2,25 +2,48 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class WeatherGovService
|
class WeatherGovService
|
||||||
{
|
{
|
||||||
public function fetchOfficesList($weatherOffice, $gridX, $gridY): array
|
private string $apiBaseUrl = "https://api.weather.gov";
|
||||||
|
|
||||||
|
private function fetchFromRemoteApi(string $url): ?array
|
||||||
{
|
{
|
||||||
//
|
$response = Http::retry(3, 60000, throw: false)->acceptJson()->get($url);
|
||||||
|
return $response->json();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchHourlyReport($weatherOffice, $gridX, $gridY): array
|
public function fetchOffice($latitude, $longitude): array
|
||||||
{
|
{
|
||||||
//
|
$apiUrl = "{$this->apiBaseUrl}/points/{$latitude}/{$longitude}";
|
||||||
|
return Cache::remember('lookup.offices', 86400, function () use ($apiUrl) {
|
||||||
|
return $this->fetchFromRemoteApi($apiUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchDailyReport($weatherOffice, $gridX, $gridY): array
|
public function fetchHourlyReport(string $weatherOffice, int $gridX, int $gridY): array
|
||||||
{
|
{
|
||||||
//
|
$apiUrl = "{$this->apiBaseUrl}/gridpoints/{$weatherOffice}/{$gridX}/{$gridY}/forecast/hourly";
|
||||||
|
return Cache::remember('lookup.offices', 900, function () use ($apiUrl) {
|
||||||
|
return $this->fetchFromRemoteApi($apiUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchWeeklyReport($weatherOffice, $gridX, $gridY): array
|
public function fetchDailyReport(string $weatherOffice, int $gridX, int $gridY): array
|
||||||
{
|
{
|
||||||
//
|
$apiUrl = "{$this->apiBaseUrl}/gridpoints/{$weatherOffice}/{$gridX}/{$gridY}/";
|
||||||
|
return Cache::remember('lookup.offices', 3600, function () use ($apiUrl) {
|
||||||
|
return $this->fetchFromRemoteApi($apiUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchWeeklyReport(string $weatherOffice, int $gridX, int $gridY): array
|
||||||
|
{
|
||||||
|
$apiUrl = "{$this->apiBaseUrl}/gridpoints/{$weatherOffice}/{$gridX}/{$gridY}/forecast";
|
||||||
|
return Cache::remember('lookup.offices', 86400, function () use ($apiUrl) {
|
||||||
|
return $this->fetchFromRemoteApi($apiUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
|||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
@@ -17,6 +18,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
AddLinkHeadersForPreloadedAssets::class,
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Ensure API routes are stateless (no sessions/cookies)
|
||||||
|
$middleware->api(remove: [
|
||||||
|
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
//
|
||||||
|
|
||||||
User::factory()->create([
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,3 +44,16 @@
|
|||||||
.weeklyReport {
|
.weeklyReport {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-shadow {
|
||||||
|
text-shadow:
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.5),
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-shadow-lg {
|
||||||
|
text-shadow:
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.6),
|
||||||
|
0 4px 16px rgba(0, 0, 0, 0.4),
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,53 @@ function parseWindSpeed(windSpeed: string | null): number {
|
|||||||
const match = windSpeed.match(/(\d+)/);
|
const match = windSpeed.match(/(\d+)/);
|
||||||
return match ? parseInt(match[1]) : 0;
|
return match ? parseInt(match[1]) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sunrise/sunset times (in hours, 24h format)
|
||||||
|
const sunriseHour = 6.75; // 6:45 AM
|
||||||
|
const sunsetHour = 17.5; // 5:30 PM
|
||||||
|
|
||||||
|
const sunPosition = computed(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const currentHour = now.getHours() + now.getMinutes() / 60;
|
||||||
|
|
||||||
|
// Calculate progress through daylight hours (0 = sunrise, 1 = sunset)
|
||||||
|
let t = (currentHour - sunriseHour) / (sunsetHour - sunriseHour);
|
||||||
|
|
||||||
|
// Clamp t between 0 and 1 for daytime, outside for night
|
||||||
|
const isDaytime = t >= 0 && t <= 1;
|
||||||
|
|
||||||
|
if (!isDaytime) {
|
||||||
|
// Nighttime: show moon at top of arc
|
||||||
|
return { cx: 100, cy: 10, isNight: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quadratic bezier curve: M 10 70 Q 100 -10 190 70
|
||||||
|
// Start point (P0): (10, 70)
|
||||||
|
// Control point (P1): (100, -10)
|
||||||
|
// End point (P2): (190, 70)
|
||||||
|
const p0 = { x: 10, y: 70 };
|
||||||
|
const p1 = { x: 100, y: -10 };
|
||||||
|
const p2 = { x: 190, y: 70 };
|
||||||
|
|
||||||
|
// Quadratic bezier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
|
||||||
|
const oneMinusT = 1 - t;
|
||||||
|
const cx = oneMinusT * oneMinusT * p0.x + 2 * oneMinusT * t * p1.x + t * t * p2.x;
|
||||||
|
const cy = oneMinusT * oneMinusT * p0.y + 2 * oneMinusT * t * p1.y + t * t * p2.y;
|
||||||
|
|
||||||
|
return { cx: Math.round(cx), cy: Math.round(cy), isNight: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedSunrise = computed(() => {
|
||||||
|
const hours = Math.floor(sunriseHour);
|
||||||
|
const minutes = Math.round((sunriseHour - hours) * 60);
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, '0')} AM`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedSunset = computed(() => {
|
||||||
|
const hours = Math.floor(sunsetHour) - 12;
|
||||||
|
const minutes = Math.round((sunsetHour - Math.floor(sunsetHour)) * 60);
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, '0')} PM`;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -108,9 +155,9 @@ function parseWindSpeed(windSpeed: string | null): number {
|
|||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<div class="relative z-10 mx-auto min-h-screen max-w-6xl p-6 lg:p-8">
|
<div class="relative z-10 mx-auto min-h-screen max-w-6xl p-6 lg:p-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="mb-8 flex items-center justify-between">
|
<header class="text-shadow mb-8 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg class="h-8 w-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-8 w-8 text-blue-400 drop-shadow-lg" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="getIconPath('cloud-sun')" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="getIconPath('cloud-sun')" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-lg font-medium text-white/80">Weather</span>
|
<span class="text-lg font-medium text-white/80">Weather</span>
|
||||||
@@ -126,26 +173,26 @@ function parseWindSpeed(windSpeed: string | null): number {
|
|||||||
<div class="currentForecast">
|
<div class="currentForecast">
|
||||||
<div class="forecast flex flex-col justify-center">
|
<div class="forecast flex flex-col justify-center">
|
||||||
<div class="shortDescription">
|
<div class="shortDescription">
|
||||||
<h1 class="text-4xl font-light tracking-wide text-white md:text-5xl">
|
<h1 class="text-shadow-lg text-4xl font-light tracking-wide text-white md:text-5xl">
|
||||||
{{ current.shortForecast }}
|
{{ current.shortForecast }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-2 text-lg text-white/60">
|
<p class="text-shadow mt-2 text-lg text-white/70">
|
||||||
{{ current.detailedForecast || 'Current conditions' }}
|
{{ current.detailedForecast || 'Current conditions' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="currentTemp mt-8">
|
<div class="currentTemp mt-8">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<span class="text-8xl font-thin tracking-tighter text-white md:text-9xl">
|
<span class="text-shadow-lg text-8xl font-thin tracking-tighter text-white md:text-9xl">
|
||||||
{{ current.temperature }}
|
{{ current.temperature }}
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-4 text-4xl font-thin text-white/60">
|
<span class="text-shadow-lg mt-4 text-4xl font-thin text-white/70">
|
||||||
°{{ current.temperatureUnit }}
|
°{{ current.temperatureUnit }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-center gap-4 text-white/60">
|
<div class="text-shadow mt-4 flex items-center gap-4 text-white/70">
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4 drop-shadow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -216,19 +263,28 @@ function parseWindSpeed(windSpeed: string | null): number {
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-dasharray="4 4"
|
stroke-dasharray="4 4"
|
||||||
/>
|
/>
|
||||||
<!-- Sun position indicator -->
|
<!-- Sun position indicator (daytime) -->
|
||||||
<circle
|
<circle
|
||||||
:cx="current.isDaytime ? 100 : 30"
|
v-if="!sunPosition.isNight"
|
||||||
:cy="current.isDaytime ? 20 : 60"
|
:cx="sunPosition.cx"
|
||||||
|
:cy="sunPosition.cy"
|
||||||
r="8"
|
r="8"
|
||||||
fill="rgb(250, 204, 21)"
|
fill="rgb(250, 204, 21)"
|
||||||
|
class="drop-shadow-lg transition-all duration-1000"
|
||||||
|
/>
|
||||||
|
<!-- Moon indicator (nighttime) -->
|
||||||
|
<g v-else :transform="`translate(${sunPosition.cx - 8}, ${sunPosition.cy - 8})`" class="transition-all duration-1000">
|
||||||
|
<path
|
||||||
|
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8c1.1 0 2.1-.2 3-.6-1.8-1.1-3-3.1-3-5.4s1.2-4.3 3-5.4C10.1.2 9.1 0 8 0z"
|
||||||
|
fill="rgb(226, 232, 240)"
|
||||||
class="drop-shadow-lg"
|
class="drop-shadow-lg"
|
||||||
/>
|
/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-xs text-white/40">
|
<div class="flex justify-between text-xs text-white/40">
|
||||||
<span>6:45 AM</span>
|
<span>{{ formattedSunrise }}</span>
|
||||||
<span>5:30 PM</span>
|
<span>{{ formattedSunset }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
6
routes/api.php
Normal file
6
routes/api.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\WeatherController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('/{period?}', [WeatherController::class, 'index'])->name('api.weather');
|
||||||
Reference in New Issue
Block a user