feat: Implement initial PolyWeather dashboard with city list, detail view, map, and API endpoints.
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const API_BASE =
|
||||
process.env.POLYWEATHER_API_BASE_URL || "http://127.0.0.1:8000";
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
|
||||
export async function GET() {
|
||||
if (!API_BASE) {
|
||||
return NextResponse.json(
|
||||
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/cities`, {
|
||||
headers: { Accept: "application/json" },
|
||||
next: { revalidate: 120 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const raw = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}` },
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const API_BASE =
|
||||
process.env.POLYWEATHER_API_BASE_URL || "http://127.0.0.1:8000";
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ name: string }> },
|
||||
) {
|
||||
if (!API_BASE) {
|
||||
return NextResponse.json(
|
||||
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const { name } = await context.params;
|
||||
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
|
||||
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}`;
|
||||
@@ -17,8 +23,9 @@ export async function GET(
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const raw = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}` },
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: 222 47% 6%;
|
||||
--background: 223 53% 4%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222 47% 9%;
|
||||
--card: 223 46% 8%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--primary: 190 95% 52%;
|
||||
--primary: 190 95% 56%;
|
||||
--primary-foreground: 222 47% 8%;
|
||||
--secondary: 223 36% 16%;
|
||||
--secondary: 224 30% 14%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--accent: 217 33% 18%;
|
||||
--accent: 217 30% 18%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--border: 220 33% 20%;
|
||||
--border: 221 38% 22%;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -21,9 +21,11 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Sora", "Avenir Next", "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 15% 5%, rgba(6, 78, 99, 0.2), transparent 45%),
|
||||
radial-gradient(circle at 85% 85%, rgba(30, 41, 59, 0.4), transparent 42%),
|
||||
radial-gradient(circle at 10% -10%, rgba(34, 211, 238, 0.2), transparent 40%),
|
||||
radial-gradient(circle at 90% 0%, rgba(59, 130, 246, 0.14), transparent 36%),
|
||||
radial-gradient(circle at 80% 100%, rgba(8, 47, 73, 0.5), transparent 48%),
|
||||
hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
@@ -33,3 +35,73 @@ body {
|
||||
height: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
background: rgba(2, 6, 23, 0.82) !important;
|
||||
border-color: rgba(71, 85, 105, 0.5) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: rgba(15, 23, 42, 0.95) !important;
|
||||
}
|
||||
|
||||
.map-pill {
|
||||
min-width: 52px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 5px 10px;
|
||||
color: #f8fafc;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow:
|
||||
0 2px 14px rgba(2, 6, 23, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.map-pill.high {
|
||||
background: linear-gradient(135deg, #ef4444, #b91c1c);
|
||||
}
|
||||
|
||||
.map-pill.medium {
|
||||
background: linear-gradient(135deg, #f59e0b, #b45309);
|
||||
}
|
||||
|
||||
.map-pill.low {
|
||||
background: linear-gradient(135deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.map-pill.active {
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(34, 211, 238, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(15, 23, 42, 0.9) 0%,
|
||||
rgba(2, 6, 23, 0.75) 100%
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.fade-up {
|
||||
animation: fadeUp 450ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user