Refactor, move all pages to separate folder

This commit is contained in:
Guntis Smaukstelis
2025-03-14 11:31:47 +02:00
parent 968a06c0f0
commit a2d1608cb0
54 changed files with 95 additions and 104 deletions
+60
View File
@@ -0,0 +1,60 @@
import moment from "moment";
import { createSignal } from "solid-js";
import { SelectCity } from "../../components/SelectCity";
import { SelectTimeRange } from "../../components/SelectTimeRange";
import { SelectField } from "../../components/SelectField";
import { SelectGranularity } from "./SelectGranularity";
import { SelectKey } from "./SelectKey";
import { QueryView } from "./result/QueryView";
const nowRounded = new Date(new Date().setMinutes(30));
const dayAgo = moment(nowRounded).subtract(1, "days").subtract(30, "minutes").toDate();
export function Cities() {
const [getCities, setCities] = createSignal<Set<string>>(new Set([]));
const [getStart, setStart] = createSignal(dayAgo);
const [getEnd, setEnd] = createSignal(nowRounded);
const [getField, setField] = createSignal("tempMax");
const [getKey, setKey] = createSignal("max");
const [getGranularity, setGranularity] = createSignal("hour");
return (
<div class="aggregator">
<div class="container">
<div class="column">
<SelectCity getCities={getCities} setCities={setCities} />
</div>
<div class="column">
<div>
<SelectTimeRange
getStart={getStart}
setStart={setStart}
getEnd={getEnd}
setEnd={setEnd}
/>
</div>
<div>
<div>
<h4>Select field</h4>
<SelectField getField={getField} setField={setField} />
</div>
<SelectKey getKey={getKey} setKey={setKey} />
<SelectGranularity getGranularity={getGranularity} setGranularity={setGranularity} />
</div>
</div>
<div class="column">
<QueryView
getCities={getCities}
getStart={getStart}
getEnd={getEnd}
getField={getField}
getKey={getKey}
getGranularity={getGranularity}
/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,22 @@
import { Accessor, Component, Setter } from "solid-js";
import { aggregateGranularity } from "../../consts";
export const SelectGranularity: Component<{
getGranularity: Accessor<string>,
setGranularity: Setter<string>,
}> = ({ getGranularity, setGranularity }) => {
return (
<div>
<h4>Select granularity</h4>
<select onChange={(e) => setGranularity(e.target.value)}>
{ aggregateGranularity.map(granularity =>
<option
value={granularity}
selected={granularity === getGranularity() ? true : false}
>{granularity}</option>
)}
</select>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { Accessor, Component, Setter } from "solid-js";
import { aggregateKey } from "../../consts";
export const SelectKey: Component<{
getKey: Accessor<string>,
setKey: Setter<string>,
}> = (props) => {
return (
<div>
<h4>Select key</h4>
<ul>
{ aggregateKey.map(key =>
<label><li><input
type="checkbox"
name="aggregateKey"
value={key}
checked={key === props.getKey() ? true : false}
onClick={() => props.setKey(key)}
/>{key}</li></label>
)}
</ul>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { Accessor, Component, Setter } from "solid-js";
import { resultOrder, ResultOrderKeys } from "../../consts";
export const SelectOrder: Component<{
getter: Accessor<ResultOrderKeys>,
setter: Setter<ResultOrderKeys>,
}> = ({ getter, setter }) => {
return (
<div>
Order by:
<select onChange={e => setter(e.target.value as ResultOrderKeys)}>
{ Object.keys(resultOrder).map(orderKey =>
<option
value={orderKey}
selected={orderKey === getter()}
>
{orderKey}
</option>
)}
</select>
</div>
);
};
+47
View File
@@ -0,0 +1,47 @@
import moment from "moment";
export function formatDateString(str: string): string {
const date = moment(new Date(str));
switch (str.length) {
// year: 2023
case 4: return date.format("yyyy");
// year-month: 2023-06
case 7: return date.format("yyyy-MM");
// year-month-day: 2023-06-23
case 10: return date.format("yyyy-MM-DD");
// year-month-day hour:minute 2023-06-23T23:59
default: return date.format("HH:mm");
}
}
export interface QueryResult {
query: DataQuery;
result: {[key: string]: any};
}
export interface DataQuery {
cities: string[];
field: string;
granularity: string;
key: string;
}
export function isQueryResult(variable: any): variable is QueryResult {
return variable
&& variable.query
&& isDataQuery(variable.query)
&& variable.result
&& (
typeof variable.result === "object"
&& !Array.isArray(variable.result)
&& variable.result !== null
);
}
export function isDataQuery(variable: any): variable is DataQuery {
return variable &&
Array.isArray(variable.cities) &&
typeof variable.field === 'string' &&
typeof variable.granularity === 'string' &&
typeof variable.key === 'string';
}
@@ -0,0 +1,36 @@
import { Component, Match, Switch } from "solid-js";
import { CityChart } from "../../../components/chart/CityChart";
import { DataQuery } from "../helpers";
export const GridResult: Component<{ city: string, query: DataQuery, result: any }> = ({ city, query, result }) => {
return (
<div class="item">
<h4>{ city }</h4>
<Switch fallback={<p>{ result }</p>}>
<Match when={query.field === "phenomena"}>
{ result.join(", ") }
</Match>
<Match when={isDateNumber(result)}>
<CityChart city={()=>city} data={()=>result} query={()=>query}/>
</Match>
</Switch>
</div>
);
}
function isDateNumber(value: unknown): value is [string, number | null][] {
if (!Array.isArray(value)) return false;
for (let i = 0; i < value.length; i++) {
let item = value[i];
if (
!Array.isArray(item)
|| item.length !== 2
|| typeof item[0] !== "string"
|| (typeof item[1] !== "number" && item[1] !== null)
) {
return false;
}
}
return true;
}
+53
View File
@@ -0,0 +1,53 @@
import moment from 'moment'
import { Accessor, Component, createResource, createSignal } from "solid-js";
import "../../../css/Result.css"
import { FETCH_DELAY_MS, apiHost } from "../../../consts";
import { isQueryResult } from "../helpers";
import { LoadingSpinner } from "../../../components/spinner/LoadingSpinner";
import { ResultView } from "./ResultView";
export const QueryView: Component<{
getCities: Accessor<Set<string>>,
getStart: Accessor<Date>,
getEnd: Accessor<Date>,
getField: Accessor<string>,
getKey: Accessor<string>,
getGranularity: Accessor<string>,
}> = (props) => {
const cities = () => [...props.getCities()].join(",");
const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm");
const queryEnd = () => moment(props.getEnd()).format("YYYYMMDD_HHmm");
const [getTimestamp, setTimestamp] = createSignal<number>(0);
const fetchQuery = async (timestamp: number) => {
if (cities() === "") return new Error("ERROR: Select cities!");
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();
return json;
}
const [queryResource] = createResource(getTimestamp, fetchQuery);
return (
<div>
<input
type="button"
class="primary"
value="Query Data"
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>
}
{ queryResource() && isQueryResult( queryResource() ) &&
<ResultView result={queryResource} />
}
</div>
);
}
@@ -0,0 +1,91 @@
import { Component, createSignal, JSXElement, Resource, Show } from "solid-js";
import "../../../css/Result.css"
import { ResultKeyVal, resultOrder, ResultOrderKeys } from "../../../consts";
import { SelectOrder } from "../SelectOrder";
import { QueryResult } from "../helpers";
import { MapView } from "../../../components/map/MapView";
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"];
export const ResultView: Component<{
result: Resource<QueryResult>
}> = ({ result: resultResource }) => {
const [getOrderKey, setOrderKey] = createSignal<ResultOrderKeys>("A -> Z");
const [getResultView, setResultView] = createSignal<ResultView>("grid");
const cityData = () =>
Object.entries(resultResource()!.result)
.map(keyVal => [...keyVal] as ResultKeyVal)
.sort((a, b) => resultOrder[getOrderKey()](a, b));
function showViewButton(viewName: string): boolean {
if (!resultResource()) return false;
if (["grid", "list"].includes(viewName) && resultResource()!.query.field === "phenomena") return true;
if (["grid", "list"].includes(viewName) && resultResource()!.query.key !== "List") return true;
if (!["grid", "list"].includes(viewName) && resultResource()!.query.key === "List") return false;
if (resultResource()!.query.field === "phenomena") return false;
return true;
}
return (
<div>
<div class="resultTitle">
<span>
{resultViews.map(viewName =>
<>
<input
type="button"
class="secondary"
value={viewName}
disabled={!showViewButton(viewName)}
onClick={() => setResultView(viewName)}
/>
&nbsp; | &nbsp;
</>
)}
</span>
<span style={{ visibility: ["grid", "list"].includes(getResultView()) ? "visible" : "hidden" }}>
<SelectOrder getter={getOrderKey} setter={setOrderKey} />
</span>
</div>
<div>
<Show when={getResultView() === "grid"}>
<div class="grid-view">
{cityData().map(([city, data]) =>
<GridResult
city={city}
query={resultResource()!.query}
result={data}
/>)}
</div>
</Show>
<Show when={getResultView() === "list"}>
<div class="list-view">
{cityData().map(([city, data]) =>
<GridResult
city={city}
query={resultResource()!.query}
result={data}
/>)}
</div>
</Show>
<Show when={getResultView() === "map_1920x1080"}>
<MapView type="map_1920x1080" data={cityData} />
</Show>
<Show when={getResultView() === "map_1920x1080_wind"}>
<MapView type="map_1920x1080_wind" data={cityData} />
</Show>
<Show when={getResultView() === "map_3840x1440"}>
<MapView type="map_3840x1440" data={cityData} />
</Show>
<Show when={getResultView() === "map_3840x1440_wind"}>
<MapView type="map_3840x1440_wind" data={cityData} />
</Show>
</div>
</div>
);
}