Add dashboard UI foundation and map export workflow
This commit is contained in:
@@ -21,12 +21,15 @@ export function Cities() {
|
||||
const [getGranularity, setGranularity] = createSignal("hour");
|
||||
|
||||
return (
|
||||
<div class="aggregator">
|
||||
<div class="aggregator pageWorkspace">
|
||||
<div class="pageHeading"><div><span class="eyebrow">Historical observations</span><h1>City analysis</h1><p>Choose stations and a calculation, then compare results or prepare a map graphic.</p></div><div class="selectionCount">{getCities().size} cities selected</div></div>
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
<aside class="column panel cityPanel">
|
||||
<h2>1. Choose cities</h2>
|
||||
<SelectCity getCities={getCities} setCities={setCities} />
|
||||
</div>
|
||||
<div class="column">
|
||||
</aside>
|
||||
<aside class="column panel queryPanel">
|
||||
<h2>2. Configure query</h2>
|
||||
<div>
|
||||
<SelectTimeRange
|
||||
getStart={getStart}
|
||||
@@ -43,8 +46,8 @@ export function Cities() {
|
||||
<SelectKey getKey={getKey} setKey={setKey} />
|
||||
<SelectGranularity getGranularity={getGranularity} setGranularity={setGranularity} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
</aside>
|
||||
<section class="column resultsPanel">
|
||||
<QueryView
|
||||
getCities={getCities}
|
||||
getStart={getStart}
|
||||
@@ -53,8 +56,8 @@ export function Cities() {
|
||||
getKey={getKey}
|
||||
getGranularity={getGranularity}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const QueryView: Component<{
|
||||
const [getTimestamp, setTimestamp] = createSignal<number>(0);
|
||||
|
||||
const fetchQuery = async (timestamp: number) => {
|
||||
if (cities() === "") return new Error("ERROR: Select cities!");
|
||||
if (cities() === "") return undefined;
|
||||
await new Promise(resolve => setTimeout(resolve, FETCH_DELAY_MS))
|
||||
const response = await fetch(`${apiHost}/api/query/city/${cities()}/${queryStart()}-${queryEnd()}/${props.getGranularity()}/${props.getField()}/${props.getKey()}`);
|
||||
const json = await response.json();
|
||||
@@ -36,18 +36,17 @@ export const QueryView: Component<{
|
||||
type="button"
|
||||
class="primary"
|
||||
value="Query Data"
|
||||
disabled={cities() === ""}
|
||||
onClick={() => setTimestamp(Date.now())}
|
||||
/>
|
||||
{ queryResource.loading && <LoadingSpinner text="Loading query" /> }
|
||||
{ queryResource.error && (
|
||||
<div>Error while querying: ${queryResource.error}</div>
|
||||
)}
|
||||
{ queryResource() && queryResource() instanceof Error &&
|
||||
<div>{ queryResource().message }</div>
|
||||
}
|
||||
{ cities() === "" && <div class="emptyState"><strong>Select at least one city</strong><span>Your results and map options will appear here after you choose cities and run the query.</span></div> }
|
||||
{ queryResource() && isQueryResult( queryResource() ) &&
|
||||
<ResultView result={queryResource} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { GridResult } from "./GridResult";
|
||||
|
||||
type ResultView = "grid" | "list" | "map_1920x1080" | "map_1920x1080_wind" | "map_3840x1440" | "map_3840x1440_wind";
|
||||
const resultViews: Array<ResultView> = ["grid", "list", "map_1920x1080", "map_1920x1080_wind", "map_3840x1440", "map_3840x1440_wind"];
|
||||
const viewLabels: Record<ResultView,string> = {grid:'Cards',list:'List',map_1920x1080:'HD map',map_1920x1080_wind:'HD map + wind',map_3840x1440:'Ultrawide map',map_3840x1440_wind:'Ultrawide + wind'}
|
||||
|
||||
export const ResultView: Component<{
|
||||
result: Resource<QueryResult>
|
||||
@@ -40,11 +41,10 @@ export const ResultView: Component<{
|
||||
<input
|
||||
type="button"
|
||||
class="secondary"
|
||||
value={viewName}
|
||||
value={viewLabels[viewName]}
|
||||
disabled={!showViewButton(viewName)}
|
||||
onClick={() => setResultView(viewName)}
|
||||
/>
|
||||
|
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -88,4 +88,4 @@ export const ResultView: Component<{
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { A } from '@solidjs/router'
|
||||
import './home.css'
|
||||
|
||||
const tools = [
|
||||
['/station','Monitor','Stations','Inspect recent measurements on a map and open detailed station charts.','◉'],
|
||||
['/cities','Analyse & export','City analysis','Compare cities, aggregate values, and prepare map graphics.','▦'],
|
||||
['/latvia','Summarise','Latvia overview','Review country-wide minimum, maximum, average, and total values.','⌁'],
|
||||
['/database','Browse','Data archive','Find available dates and inspect individual hourly records.','▤'],
|
||||
['/harmonie','Forecast model','HARMONIE','Render forecast fields from locally stored HARMONIE GRIB files.','◫'],
|
||||
['/lvgmc-forecast','Forecast products','LVĢMC forecast','Open prepared LVĢMC city forecasts and broadcast tables.','☁'],
|
||||
]
|
||||
export function Home() { return <section class="homePage"><div class="homeHero"><div><p class="eyebrow">Weather data and production tools</p><h1>Choose a workspace</h1><p class="heroCopy">Explore station history, compare cities, inspect forecasts, or prepare weather graphics from one place.</p></div><div class="environmentBadge"><span class="statusDot"/>Development dataset<small>Synthetic station observations are active</small></div></div><div class="toolGrid">{tools.map(([path,eyebrow,title,description,icon]) => <A class="toolCard" href={path}><span class="toolIcon">{icon}</span><span class="eyebrow">{eyebrow}</span><h2>{title}</h2><p>{description}</p><span class="cardAction">Open workspace →</span></A>)}</div></section> }
|
||||
@@ -0,0 +1 @@
|
||||
.homePage{max-width:1440px;margin:auto;padding:56px 32px 80px}.homeHero{display:flex;justify-content:space-between;align-items:end;gap:48px;margin-bottom:40px}.homeHero h1{margin:8px 0 14px;font-size:clamp(38px,5vw,68px);line-height:.98;letter-spacing:-.045em}.heroCopy{max-width:720px;margin:0;color:var(--text-muted);font-size:18px;line-height:1.6}.eyebrow{color:var(--accent-strong);font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.environmentBadge{min-width:250px;padding:16px 18px;border:1px solid var(--border);border-radius:14px;background:var(--surface);font-weight:700;box-shadow:var(--shadow-sm)}.environmentBadge small{display:block;margin:5px 0 0 18px;color:var(--text-muted);font-weight:400}.statusDot{display:inline-block;width:9px;height:9px;margin-right:9px;border-radius:50%;background:var(--success);box-shadow:0 0 0 4px rgba(38,147,104,.12)}.toolGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.toolCard{position:relative;min-height:230px;padding:26px;overflow:hidden;border:1px solid var(--border);border-radius:18px;background:var(--surface);color:var(--text);text-decoration:none;box-shadow:var(--shadow-sm);transition:.18s}.toolCard:hover{transform:translateY(-3px);border-color:var(--accent-soft);box-shadow:var(--shadow-md)}.toolCard h2{margin:28px 0 10px;font-size:24px}.toolCard p{max-width:90%;margin:0;color:var(--text-muted);line-height:1.55}.toolIcon{position:absolute;top:20px;right:22px;color:var(--accent);font-size:31px}.cardAction{position:absolute;bottom:24px;left:26px;font-weight:700}@media(max-width:960px){.toolGrid{grid-template-columns:repeat(2,1fr)}.homeHero{align-items:start;flex-direction:column}}@media(max-width:640px){.homePage{padding:36px 18px}.toolGrid{grid-template-columns:1fr}}
|
||||
Reference in New Issue
Block a user