chore: install project dependencies and generate build artifacts.

This commit is contained in:
2569718930@qq.com
2026-03-06 08:41:19 +08:00
parent b210dcf0dd
commit dc4167b514
28 changed files with 4143 additions and 0 deletions
+5
View File
@@ -25,3 +25,8 @@ pip-delete-this-directory.txt
# OS
.DS_Store
Thumbs.db
# Next.js frontend
frontend/node_modules/
frontend/.next/
frontend/.vercel/
+1
View File
@@ -0,0 +1 @@
POLYWEATHER_API_BASE_URL=http://127.0.0.1:8000
+50
View File
@@ -0,0 +1,50 @@
# PolyWeather Frontend (Next.js)
Standalone web frontend for `polyweather.vercel.app`.
## Stack
- Next.js 14+ (App Router)
- Tailwind CSS
- Lucide React
- shadcn/ui base components
- Leaflet (react-leaflet)
## Local Development
```bash
cd frontend
cp .env.example .env.local
npm install
npm run dev
```
Default frontend URL: `http://localhost:3000`
## Backend API
Set `POLYWEATHER_API_BASE_URL` to your FastAPI service URL.
Example:
```bash
POLYWEATHER_API_BASE_URL=https://api.yourdomain.com
```
The frontend uses Next Route Handlers as a thin BFF layer:
- `GET /api/cities`
- `GET /api/city/:name`
## Vercel Deployment
1. Import this repo in Vercel.
2. Set **Root Directory** to `frontend`.
3. Add environment variable:
- `POLYWEATHER_API_BASE_URL=https://<your-fastapi-host>`
4. Deploy.
## Notes
- Backend CORS must allow `https://polyweather.vercel.app`.
- This is phase-1 split: map + city list + detail panel are migrated first.
+26
View File
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
const API_BASE =
process.env.POLYWEATHER_API_BASE_URL || "http://127.0.0.1:8000";
export async function GET() {
try {
const res = await fetch(`${API_BASE}/api/cities`, {
headers: { Accept: "application/json" },
next: { revalidate: 120 },
});
if (!res.ok) {
return NextResponse.json(
{ error: `Backend returned ${res.status}` },
{ status: 502 },
);
}
const data = await res.json();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch cities", detail: String(error) },
{ status: 500 },
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
const API_BASE =
process.env.POLYWEATHER_API_BASE_URL || "http://127.0.0.1:8000";
export async function GET(
req: NextRequest,
context: { params: Promise<{ name: string }> },
) {
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}`;
try {
const res = await fetch(url, {
headers: { Accept: "application/json" },
cache: "no-store",
});
if (!res.ok) {
return NextResponse.json(
{ error: `Backend returned ${res.status}` },
{ status: 502 },
);
}
const data = await res.json();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch city detail", detail: String(error) },
{ status: 500 },
);
}
}
+35
View File
@@ -0,0 +1,35 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 222 47% 6%;
--foreground: 210 40% 98%;
--card: 222 47% 9%;
--card-foreground: 210 40% 98%;
--primary: 190 95% 52%;
--primary-foreground: 222 47% 8%;
--secondary: 223 36% 16%;
--secondary-foreground: 210 40% 98%;
--accent: 217 33% 18%;
--accent-foreground: 210 40% 98%;
--border: 220 33% 20%;
}
* {
border-color: hsl(var(--border));
}
body {
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%),
hsl(var(--background));
color: hsl(var(--foreground));
}
.leaflet-container {
width: 100%;
height: 100%;
font-family: inherit;
}
+20
View File
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "PolyWeather | Weather Intelligence",
description:
"PolyWeather pro dashboard with global weather risk map and city analytics.",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN" className="dark">
<body className="min-h-screen font-sans antialiased">
{children}
</body>
</html>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { PolyWeatherDashboard } from "@/components/polyweather-dashboard";
export default function HomePage() {
return <PolyWeatherDashboard />;
}
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+117
View File
@@ -0,0 +1,117 @@
"use client";
import { CloudSun, Gauge, Loader2, RefreshCw, Thermometer } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { CityDetail } from "@/lib/types";
type CityDetailPanelProps = {
detail: CityDetail | null;
loading: boolean;
onRefresh: () => void;
};
export function CityDetailPanel({
detail,
loading,
onRefresh,
}: CityDetailPanelProps) {
if (!detail) {
return (
<Card className="h-full">
<CardHeader>
<CardTitle>City Detail</CardTitle>
</CardHeader>
<CardContent className="text-sm text-slate-400">
Select a city from the left list or map marker.
</CardContent>
</Card>
);
}
const symbol = detail.temp_symbol || "C";
const displayTemp =
detail.current?.max_so_far != null
? detail.current.max_so_far
: detail.current?.temp;
return (
<Card className="h-full">
<CardHeader className="space-y-3">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-xl uppercase tracking-wide text-cyan-200">
{detail.display_name}
</CardTitle>
<Button
size="sm"
variant="secondary"
onClick={onRefresh}
disabled={loading}
>
{loading ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
)}
Refresh
</Button>
</div>
<div className="flex items-center gap-2">
<Badge variant="default">{detail.local_time || "--:--"}</Badge>
{detail.current?.wu_settlement != null ? (
<Badge variant="warning">WU {detail.current.wu_settlement}</Badge>
) : null}
</div>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div className="rounded-lg border border-slate-800 bg-slate-900/70 p-3">
<div className="mb-1 flex items-center gap-2 text-slate-400">
<Thermometer className="h-4 w-4" />
<span>Current / Max</span>
</div>
<div className="text-2xl font-semibold text-white">
{displayTemp ?? "--"}
{symbol}
</div>
<div className="mt-1 text-xs text-slate-400">
DEB: {detail.deb?.prediction ?? "--"}
{symbol}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border border-slate-800 bg-slate-900/70 p-3">
<div className="mb-1 flex items-center gap-2 text-slate-400">
<CloudSun className="h-4 w-4" />
<span>Cloud</span>
</div>
<p className="font-medium">{detail.current?.cloud_desc || "N/A"}</p>
</div>
<div className="rounded-lg border border-slate-800 bg-slate-900/70 p-3">
<div className="mb-1 flex items-center gap-2 text-slate-400">
<Gauge className="h-4 w-4" />
<span>Wind</span>
</div>
<p className="font-medium">
{detail.current?.wind_speed_kt != null
? `${detail.current.wind_speed_kt} kt`
: "N/A"}
</p>
</div>
</div>
<div className="rounded-lg border border-slate-800 bg-slate-900/70 p-3">
<p className="mb-2 text-slate-400">AI Summary</p>
<div
className="max-h-44 overflow-y-auto leading-6 text-slate-200"
dangerouslySetInnerHTML={{
__html: detail.ai_analysis || "No analysis yet.",
}}
/>
</div>
</CardContent>
</Card>
);
}
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { ThermometerSun } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import type { CitySummary } from "@/lib/types";
type CityListProps = {
cities: CitySummary[];
selectedCity: string | null;
onSelectCity: (cityName: string) => void;
};
function riskVariant(level: CitySummary["risk_level"]): "success" | "warning" | "danger" {
if (level === "high") return "danger";
if (level === "medium") return "warning";
return "success";
}
export function CityList({ cities, selectedCity, onSelectCity }: CityListProps) {
return (
<Card className="h-full">
<CardHeader className="pb-3">
<CardTitle className="flex items-center justify-between text-sm uppercase tracking-wider text-slate-300">
<span>Monitored Cities</span>
<Badge variant="default">{cities.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="h-[calc(100%-64px)] overflow-y-auto">
<div className="space-y-2">
{cities.map((city) => (
<button
key={city.name}
onClick={() => onSelectCity(city.name)}
className={cn(
"w-full rounded-lg border px-3 py-2 text-left transition-colors",
selectedCity === city.name
? "border-cyan-500 bg-cyan-950/40"
: "border-slate-800 bg-slate-900/70 hover:bg-slate-800/80",
)}
>
<div className="flex items-center justify-between gap-2">
<div className="truncate font-medium text-slate-100">
{city.display_name}
</div>
<Badge variant={riskVariant(city.risk_level)}>{city.risk_level}</Badge>
</div>
<div className="mt-1 flex items-center gap-1 text-xs text-slate-400">
<ThermometerSun className="h-3.5 w-3.5" />
<span>{city.temp_unit === "fahrenheit" ? "Fahrenheit" : "Celsius"}</span>
</div>
</button>
))}
</div>
</CardContent>
</Card>
);
}
+55
View File
@@ -0,0 +1,55 @@
"use client";
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import type { CitySummary } from "@/lib/types";
type MapCanvasProps = {
cities: CitySummary[];
selectedCity: string | null;
onSelectCity: (cityName: string) => void;
};
const tempIcon = (risk: CitySummary["risk_level"]) =>
L.divIcon({
className: "",
html: `<div style="
background:${risk === "high" ? "#ef4444" : risk === "medium" ? "#f59e0b" : "#10b981"};
color:#fff;
padding:6px 10px;
border-radius:999px;
font-size:12px;
font-weight:700;
box-shadow:0 2px 10px rgba(0,0,0,.4);
">${risk.toUpperCase()}</div>`,
iconSize: [56, 28],
iconAnchor: [28, 14],
});
export function MapCanvas({ cities, selectedCity, onSelectCity }: MapCanvasProps) {
return (
<MapContainer center={[30, 10]} zoom={3} minZoom={2} className="h-full w-full">
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>'
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
/>
{cities.map((city) => (
<Marker
key={city.name}
position={[city.lat, city.lon]}
icon={tempIcon(city.risk_level)}
eventHandlers={{ click: () => onSelectCity(city.name) }}
>
<Popup>
<div className="text-sm">
<p className="font-semibold">{city.display_name}</p>
<p>Risk: {city.risk_level}</p>
<p>{selectedCity === city.name ? "Selected" : "Click to open"}</p>
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import dynamic from "next/dynamic";
import type { CitySummary } from "@/lib/types";
const DynamicMap = dynamic(
() => import("@/components/map-canvas").then((m) => m.MapCanvas),
{ ssr: false },
);
type MapViewProps = {
cities: CitySummary[];
selectedCity: string | null;
onSelectCity: (cityName: string) => void;
};
export function MapView(props: MapViewProps) {
return (
<div className="h-full w-full overflow-hidden rounded-2xl border border-slate-800">
<DynamicMap {...props} />
</div>
);
}
@@ -0,0 +1,135 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { AlertTriangle, Bot, Globe2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CityDetailPanel } from "@/components/city-detail-panel";
import { CityList } from "@/components/city-list";
import { MapView } from "@/components/map-view";
import { getCities, getCityDetail } from "@/lib/api";
import type { CityDetail, CitySummary } from "@/lib/types";
export function PolyWeatherDashboard() {
const [cities, setCities] = useState<CitySummary[]>([]);
const [selectedCity, setSelectedCity] = useState<string | null>(null);
const [selectedDetail, setSelectedDetail] = useState<CityDetail | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
(async () => {
try {
setError(null);
const data = await getCities();
if (!mounted) return;
setCities(data);
} catch (err) {
if (!mounted) return;
setError(String(err));
}
})();
return () => {
mounted = false;
};
}, []);
useEffect(() => {
if (!selectedCity) return;
let mounted = true;
(async () => {
try {
setLoading(true);
setError(null);
const detail = await getCityDetail(selectedCity);
if (!mounted) return;
setSelectedDetail(detail);
} catch (err) {
if (!mounted) return;
setError(String(err));
} finally {
if (mounted) setLoading(false);
}
})();
return () => {
mounted = false;
};
}, [selectedCity]);
const orderedCities = useMemo(() => {
const order = { high: 0, medium: 1, low: 2 };
return [...cities].sort(
(a, b) => (order[a.risk_level] ?? 99) - (order[b.risk_level] ?? 99),
);
}, [cities]);
async function refreshCurrentCity() {
if (!selectedCity) return;
setLoading(true);
try {
const detail = await getCityDetail(selectedCity, true);
setSelectedDetail(detail);
} catch (err) {
setError(String(err));
} finally {
setLoading(false);
}
}
return (
<main className="h-screen w-full p-3 md:p-4">
<div className="grid h-full grid-cols-1 gap-3 md:grid-cols-[280px_1fr_360px]">
<section className="min-h-0">
<CityList
cities={orderedCities}
selectedCity={selectedCity}
onSelectCity={setSelectedCity}
/>
</section>
<section className="relative min-h-0">
<div className="absolute left-3 right-3 top-3 z-[900] rounded-xl border border-slate-800 bg-slate-950/70 p-2 backdrop-blur md:left-4 md:right-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Globe2 className="h-4 w-4 text-cyan-300" />
<h1 className="text-lg font-semibold uppercase tracking-wide text-cyan-100 md:text-xl">
PolyWeather
</h1>
<span className="hidden text-xs text-slate-400 md:inline">
Global Weather Risk Intelligence
</span>
</div>
<Button size="sm" variant="secondary">
<Bot className="mr-1 h-4 w-4" />
Technical Guide
</Button>
</div>
</div>
<MapView
cities={orderedCities}
selectedCity={selectedCity}
onSelectCity={setSelectedCity}
/>
</section>
<section className="min-h-0">
<CityDetailPanel
detail={selectedDetail}
loading={loading}
onRefresh={refreshCurrentCity}
/>
</section>
</div>
{error ? (
<div className="pointer-events-none fixed bottom-4 left-1/2 z-[1200] -translate-x-1/2 rounded-lg border border-red-900 bg-red-950/90 px-3 py-2 text-sm text-red-100">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4" />
<span>{error}</span>
</div>
</div>
) : null}
</main>
);
}
+33
View File
@@ -0,0 +1,33 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
{
variants: {
variant: {
default: "border-cyan-700 bg-cyan-950 text-cyan-200",
success: "border-emerald-700 bg-emerald-950 text-emerald-200",
warning: "border-amber-700 bg-amber-950 text-amber-200",
danger: "border-red-700 bg-red-950 text-red-200",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
+51
View File
@@ -0,0 +1,51 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400",
{
variants: {
variant: {
default: "bg-cyan-400 text-slate-950 hover:bg-cyan-300",
secondary:
"bg-slate-800 text-slate-100 border border-slate-700 hover:bg-slate-700",
ghost: "hover:bg-slate-800 hover:text-slate-100",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3",
lg: "h-10 rounded-md px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+59
View File
@@ -0,0 +1,59 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border border-slate-800 bg-slate-950/70 text-slate-100 shadow-glow backdrop-blur-sm",
className,
)}
{...props}
/>
),
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-4 md:p-5", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-slate-400", className)} {...props} />
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-4 pt-0 md:p-5 md:pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
export { Card, CardHeader, CardTitle, CardDescription, CardContent };
+27
View File
@@ -0,0 +1,27 @@
import type { CityDetail, CitySummary } from "@/lib/types";
export async function getCities(): Promise<CitySummary[]> {
const res = await fetch("/api/cities", { cache: "no-store" });
if (!res.ok) {
throw new Error(`Failed to load cities: ${res.status}`);
}
const data = await res.json();
return data.cities ?? [];
}
export async function getCityDetail(
cityName: string,
forceRefresh = false,
): Promise<CityDetail> {
const slug = cityName.replace(/\s/g, "-");
const res = await fetch(
`/api/city/${encodeURIComponent(slug)}?force_refresh=${forceRefresh}`,
{
cache: "no-store",
},
);
if (!res.ok) {
throw new Error(`Failed to load city detail: ${res.status}`);
}
return await res.json();
}
+35
View File
@@ -0,0 +1,35 @@
export type CitySummary = {
name: string;
display_name: string;
lat: number;
lon: number;
risk_level: "low" | "medium" | "high";
risk_emoji?: string;
temp_unit: "fahrenheit" | "celsius";
is_major?: boolean;
};
export type CityDetail = {
name: string;
display_name: string;
lat: number;
lon: number;
temp_symbol: string;
local_time: string;
current?: {
temp?: number | null;
max_so_far?: number | null;
wu_settlement?: number | null;
cloud_desc?: string | null;
wind_speed_kt?: number | null;
obs_time?: string | null;
};
deb?: {
prediction?: number | null;
};
probabilities?: {
mu?: number | null;
distribution?: Array<{ value: number; probability: number }>;
};
ai_analysis?: string;
};
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default nextConfig;
+3227
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "polyweather-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"leaflet": "^1.9.4",
"lucide-react": "^0.511.0",
"next": "^15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"tailwind-merge": "^3.3.0"
},
"devDependencies": {
"@types/leaflet": "^1.9.20",
"@types/node": "^22.15.21",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+33
View File
@@ -0,0 +1,33 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./lib/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: "hsl(var(--card))",
"card-foreground": "hsl(var(--card-foreground))",
primary: "hsl(var(--primary))",
"primary-foreground": "hsl(var(--primary-foreground))",
secondary: "hsl(var(--secondary))",
"secondary-foreground": "hsl(var(--secondary-foreground))",
accent: "hsl(var(--accent))",
"accent-foreground": "hsl(var(--accent-foreground))",
border: "hsl(var(--border))",
},
boxShadow: {
glow: "0 0 0 1px rgba(34, 211, 238, 0.2), 0 8px 32px rgba(8, 47, 73, 0.5)",
},
},
},
plugins: [],
};
export default config;
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+13
View File
@@ -18,6 +18,7 @@ if _root not in sys.path:
sys.path.insert(0, _root)
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from loguru import logger
@@ -32,6 +33,18 @@ from src.analysis.deb_algorithm import calculate_dynamic_weights, get_deb_accura
# ──────────────────────────────────────────────────────────
app = FastAPI(title="PolyWeather Map", version="1.0")
_cors_origins = os.getenv(
"WEB_CORS_ORIGINS",
"http://localhost:3000,http://127.0.0.1:3000,https://polyweather.vercel.app",
)
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in _cors_origins.split(",") if o.strip()],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
_static = os.path.join(os.path.dirname(__file__), "static")
os.makedirs(_static, exist_ok=True)
app.mount("/static", StaticFiles(directory=_static), name="static")