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 # OS
.DS_Store .DS_Store
Thumbs.db 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) sys.path.insert(0, _root)
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from loguru import logger 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") 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") _static = os.path.join(os.path.dirname(__file__), "static")
os.makedirs(_static, exist_ok=True) os.makedirs(_static, exist_ok=True)
app.mount("/static", StaticFiles(directory=_static), name="static") app.mount("/static", StaticFiles(directory=_static), name="static")