Compare commits

...

5 Commits

Author SHA1 Message Date
Araozu 1672d4de94 Move mobile UI to its own page, and redirect to it 2024-09-09 16:34:01 -05:00
Araozu b836e5e8eb Remove previous index. Create a new page for mobile users 2024-09-09 15:15:52 -05:00
Araozu 102f7a2d19 Add second route 2024-09-09 13:57:49 -05:00
Araozu 959b519d75 Naive mobile UI 2024-09-09 13:43:08 -05:00
Araozu 3012d992b0 add jenkinsfile 2024-09-09 12:06:56 -05:00
10 changed files with 354 additions and 321 deletions

23
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,23 @@
pipeline {
agent any
environment {
PATH = "/var/lib/jenkins/bin:/var/lib/jenkins/.nvm/versions/node/v20.9.0/bin:${env.PATH}"
}
stages {
stage('Install deps') {
steps {
sh 'pnpm i'
}
}
stage('Build bundle') {
steps {
sh 'pnpm build'
}
}
stage('Deploy') {
steps {
sh 'cp -r ./dist/* /var/www/combi/'
}
}
}
}

View File

@ -6,5 +6,13 @@
"color": "#ea4fb2",
"hover_bg": "#ea4fb2",
"hover_on_bg": "white"
},
{
"id": 2,
"name": "2",
"district": "Socabaya",
"color": "blue",
"hover_bg": "blue",
"hover_on_bg": "white"
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,19 @@
import "leaflet/dist/leaflet.css";
import { Index } from "./pages/Index";
import { Route, Router } from "@solidjs/router";
import { Route, HashRouter } from "@solidjs/router";
import { Editor } from "./pages/Editor";
import { Index2 } from "./pages/Index2";
import { Index } from "./pages/Index";
import { Arrow } from "./pages/Arrow";
import { IndexMobile } from "./pages/IndexMobile";
export default function() {
return (
<>
<Router>
<HashRouter>
<Route path="/" component={Index} />
<Route path="/new" component={Index2} />
<Route path="/mobile" component={IndexMobile} />
<Route path="/editor" component={Editor} />
<Route path="/arrow" component={Arrow} />
</Router>
</HashRouter>
</>
);
}

View File

@ -1,39 +1,45 @@
import { Map, map, polyline, tileLayer } from "leaflet";
import { createEffect, createResource, For, onMount, Suspense } from "solid-js";
import { Line, RouteWrapper, Route, LineWrapper, PointsWrapper } from "../types";
import L from "leaflet";
import { createEffect, createResource, createSignal, For, onMount, Suspense } from "solid-js";
import { LineSegmentsIcon } from "../icons/LineSegmentsIcon";
import { Line, Lines, Route, Routes } from "../new_types";
let g_map: Map | null = null;
let global_map: L.Map | null = null;
const [global_count, set_global_count] = createSignal(0);
export function Index() {
const container = <div class="h-[98vh] rounded-lg dark:opacity-60" />;
const container = <div class="md:h-[98vh] h-[65vh] md:rounded-lg dark:opacity-60" />;
onMount(() => {
const l_map = map(container as HTMLElement)
.setView([-16.40171, -71.53040], 13);
g_map = l_map;
// Detect screen size and ratio,
// and redirect to mobile page if neccesary
if (window.matchMedia("only screen and (max-width: 760px)").matches) {
window.location.replace("/#/mobile");
return;
}
tileLayer("/tiles/{z}/{x}/{y}.jpg", {
global_map = L.map(container as HTMLElement)
.setView([-16.40171, -71.53040], 13);
L.tileLayer("/tiles/{z}/{x}/{y}.jpg", {
maxZoom: 17,
minZoom: 12,
attribution: "&copy; Map data <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
}).addTo(l_map);
attribution: "Map data &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
}).addTo(global_map);
});
return (
<div class="grid grid-cols-[15rem_auto]">
<div class="h-screen overflow-scroll">
<div class="grid md:grid-cols-[15rem_auto] md:grid-rows-none grid-rows-[65vh_35vh]">
<div class="md:h-screen overflow-scroll">
<h1 class="text-c-primary text-center font-bold text-2xl py-4">AQP combi</h1>
<h2 class="bg-c-primary text-white py-2 px-2 font-bold text-lg">
Lineas
</h2>
<Lines />
<LinesEl />
</div>
<div class="py-[0.5vh] pr-2">
<div class="rounded-lg overflow-hidden p-1"
<div class="md:py-[0.5vh] md:pr-2">
<div class="md:rounded-lg overflow-hidden md:p-1"
style="box-shadow: inset 0 0 5px 0px var(--main)"
>
{container}
@ -43,18 +49,17 @@ export function Index() {
);
}
function Lines() {
const [lineData] = createResource<LineWrapper>(async() => {
const res = await fetch("/data/root.json");
function LinesEl() {
const [linesData] = createResource<Lines>(async() => {
const res = await fetch("/n/lines.json");
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
return (
<>
<Suspense fallback={<div>Cargando...</div>}>
<For each={lineData()?.data ?? []}>
<For each={linesData() ?? []}>
{(l) => <LineEl line={l} />}
</For>
</Suspense>
@ -63,61 +68,161 @@ function Lines() {
}
function LineEl(props: { line: Line }) {
const [lineRoute] = createResource<RouteWrapper>(async() => {
const res = await fetch(`/data/cuenca_${props.line.id_cuenca}.json`);
const [routesData] = createResource<Routes>(async() => {
const res = await fetch(`/n/routes_${props.line.id}.json`);
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
return (
<div style={`color: ${props.line.color}`}>
<div
style={`color: ${props.line.color};--hover-bg: ${props.line.hover_bg};--hover-on-bg: ${props.line.hover_on_bg}`}
>
<LineSegmentsIcon class="px-1" fill={props.line.color} />
<span class="px-2">
Linea {props.line.name ?? "A"}
<span class="px-2" title={props.line.district}>
Linea {props.line.name}
</span>
<Suspense>
<For each={lineRoute()?.data ?? []}>
{(r) => <RouteEl route={r} parent_id={props.line.id_cuenca} color={props.line.color} />}
<For each={routesData() ?? []}>
{(r) => (
<RouteEl
line_name={props.line.name}
route={r}
color={props.line.color}
/>
)}
</For>
</Suspense>
</div>
);
}
function RouteEl(props: { route: Route, parent_id: number, color: string }) {
const [points] = createResource<PointsWrapper>(async() => {
const res = await fetch(`/data/cuenca_${props.parent_id}_ruta_${props.route.id_ruta}_ida.json`);
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
const [return_points] = createResource<PointsWrapper>(async() => {
const res = await fetch(`/data/cuenca_${props.parent_id}_ruta_${props.route.id_ruta}_vuelta.json`);
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
function RouteEl(props: {
line_name: string,
route: Route,
color: string,
}) {
const [departure_active, set_departure_active] = createSignal(false);
const [return_active, set_return_active] = createSignal(false);
const [departure_count, set_departure_count] = createSignal(0);
const [return_count, set_return_count] = createSignal(0);
// Create departure and return polylines
const departure_polyline = L.polyline(props.route.departure, { color: props.color });
const return_polyline = L.polyline(props.route.return, { color: props.color });
// Rended the lines into the map
createEffect(() => {
// Render the dots into the map
if (!points()) return;
const coords = points()!.data[0]!.ruta_json;
const line = polyline(coords, { color: props.color});
line.addTo(g_map!);
if (global_count() === 0 || departure_count() > 0) {
departure_polyline.addTo(global_map!);
} else {
departure_polyline.removeFrom(global_map!);
}
});
createEffect(() => {
// Render the dots into the map
const coords = return_points()!.data[0]!.ruta_json;
const line = polyline(coords, { color: props.color});
line.addTo(g_map!);
// if global count === 0, then no route is focused.
// in that case, all routes should be rendered
if (global_count() === 0 || return_count() > 0) {
return_polyline.addTo(global_map!);
} else {
return_polyline.removeFrom(global_map!);
}
});
const toggle_departure = () => {
const currently_active = departure_active();
if (currently_active) {
set_global_count((c) => c - 1);
set_departure_count((c) => c - 1);
set_departure_active(false);
} else {
set_global_count((c) => c + 1);
set_departure_count((c) => c + 1);
set_departure_active(true);
}
};
const toggle_return = () => {
const currently_active = return_active();
if (currently_active) {
set_global_count((c) => c - 1);
set_return_count((c) => c - 1);
set_return_active(false);
} else {
set_global_count((c) => c + 1);
set_return_count((c) => c + 1);
set_return_active(true);
}
};
const departure_classes = () => {
if (departure_count() > 0) {
return "bg-r-hover-bg text-r-hover-on-bg";
} else {
return "";
}
};
const return_classes = () => {
if (return_count() > 0) {
return "bg-r-hover-bg text-r-hover-on-bg";
} else {
return "";
}
};
return (
<div class="pl-10">
Ruta {props.route.cod_ruta}
</div>
<button
class="grid grid-cols-[auto_2.75rem_3.5rem] w-full text-left cursor-pointer"
>
<span
class={"pl-10 border-2 border-transparent hover:border-r-hover-bg"}
onMouseEnter={() => {
set_global_count((c) => c + 1);
set_departure_count((c) => c + 1);
set_return_count((c) => c + 1);
}}
onMouseLeave={() => {
set_global_count((c) => c - 1);
set_departure_count((c) => c - 1);
set_return_count((c) => c - 1);
}}
onClick={() => {
if (departure_count() === 1) {
toggle_departure();
}
if (return_count() === 1) {
toggle_return();
}
}}
>
Ruta {props.route.name}
</span>
<span
class={`text-center border-2 border-transparent ${departure_classes()}`}
onMouseEnter={() => {
set_global_count((c) => c + 1);
set_departure_count((c) => c + 1);
}}
onMouseLeave={() => {
set_global_count((c) => c - 1);
set_departure_count((c) => c - 1);
}}
onClick={toggle_departure}
>
Ida
</span>
<span
class={`text-center border-2 border-transparent ${return_classes()}`}
onMouseEnter={() => {
set_global_count((c) => c + 1);
set_return_count((c) => c + 1);
}}
onMouseLeave={() => {
set_global_count((c) => c - 1);
set_return_count((c) => c - 1);
}}
onClick={toggle_return}
>
Vuelta
</span>
</button>
);
}

View File

@ -1,221 +0,0 @@
import L from "leaflet";
import { createEffect, createResource, createSignal, For, onMount, Suspense } from "solid-js";
import { LineSegmentsIcon } from "../icons/LineSegmentsIcon";
import { Line, Lines, Route, Routes } from "../new_types";
let global_map: L.Map | null = null;
const [global_count, set_global_count] = createSignal(0);
export function Index2() {
const container = <div class="md:h-[98vh] h-[65vh] md:rounded-lg dark:opacity-60" />;
onMount(() => {
global_map = L.map(container as HTMLElement)
.setView([-16.40171, -71.53040], 13);
L.tileLayer("/tiles/{z}/{x}/{y}.jpg", {
maxZoom: 17,
minZoom: 12,
attribution: "Map data &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
}).addTo(global_map);
});
return (
<div class="grid md:grid-cols-[15rem_auto] md:grid-rows-none grid-rows-[65vh_35vh]">
<div class="md:h-screen overflow-scroll">
<h1 class="text-c-primary text-center font-bold text-2xl py-4">AQP combi</h1>
<h2 class="bg-c-primary text-white py-2 px-2 font-bold text-lg">
Lineas
</h2>
<LinesEl />
</div>
<div class="md:py-[0.5vh] md:pr-2">
<div class="md:rounded-lg overflow-hidden md:p-1"
style="box-shadow: inset 0 0 5px 0px var(--main)"
>
{container}
</div>
</div>
</div>
);
}
function LinesEl() {
const [linesData] = createResource<Lines>(async() => {
const res = await fetch("/n/lines.json");
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
return (
<>
<Suspense fallback={<div>Cargando...</div>}>
<For each={linesData() ?? []}>
{(l) => <LineEl line={l} />}
</For>
</Suspense>
</>
);
}
function LineEl(props: { line: Line }) {
const [routesData] = createResource<Routes>(async() => {
const res = await fetch(`/n/routes_${props.line.id}.json`);
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
return (
<div
style={`color: ${props.line.color};--hover-bg: ${props.line.hover_bg};--hover-on-bg: ${props.line.hover_on_bg}`}
>
<LineSegmentsIcon class="px-1" fill={props.line.color} />
<span class="px-2" title={props.line.district}>
Linea {props.line.name}
</span>
<Suspense>
<For each={routesData() ?? []}>
{(r) => (
<RouteEl
line_name={props.line.name}
route={r}
color={props.line.color}
/>
)}
</For>
</Suspense>
</div>
);
}
function RouteEl(props: {
line_name: string,
route: Route,
color: string,
}) {
const [departure_active, set_departure_active] = createSignal(false);
const [return_active, set_return_active] = createSignal(false);
const [departure_count, set_departure_count] = createSignal(0);
const [return_count, set_return_count] = createSignal(0);
// Create departure and return polylines
const departure_polyline = L.polyline(props.route.departure, { color: props.color });
const return_polyline = L.polyline(props.route.return, { color: props.color });
// Rended the lines into the map
createEffect(() => {
if (global_count() === 0 || departure_count() > 0) {
departure_polyline.addTo(global_map!);
} else {
departure_polyline.removeFrom(global_map!);
}
});
createEffect(() => {
// if global count === 0, then no route is focused.
// in that case, all routes should be rendered
if (global_count() === 0 || return_count() > 0) {
return_polyline.addTo(global_map!);
} else {
return_polyline.removeFrom(global_map!);
}
});
const toggle_departure = () => {
const currently_active = departure_active();
if (currently_active) {
set_global_count((c) => c - 1);
set_departure_count((c) => c - 1);
set_departure_active(false);
} else {
set_global_count((c) => c + 1);
set_departure_count((c) => c + 1);
set_departure_active(true);
}
};
const toggle_return = () => {
const currently_active = return_active();
if (currently_active) {
set_global_count((c) => c - 1);
set_return_count((c) => c - 1);
set_return_active(false);
} else {
set_global_count((c) => c + 1);
set_return_count((c) => c + 1);
set_return_active(true);
}
};
const departure_classes = () => {
if (departure_count() > 0) {
return "bg-r-hover-bg text-r-hover-on-bg";
} else {
return "";
}
};
const return_classes = () => {
if (return_count() > 0) {
return "bg-r-hover-bg text-r-hover-on-bg";
} else {
return "";
}
};
return (
<button
class="grid grid-cols-[auto_2.75rem_3.5rem] w-full text-left cursor-pointer"
>
<span
class={"pl-10 border-2 border-transparent hover:border-r-hover-bg"}
onMouseEnter={() => {
set_global_count((c) => c + 1);
set_departure_count((c) => c + 1);
set_return_count((c) => c + 1);
}}
onMouseLeave={() => {
set_global_count((c) => c - 1);
set_departure_count((c) => c - 1);
set_return_count((c) => c - 1);
}}
onClick={() => {
if (departure_count() === 1) {
toggle_departure();
}
if (return_count() === 1) {
toggle_return();
}
}}
>
Ruta {props.route.name}
</span>
<span
class={`text-center border-2 border-transparent ${departure_classes()}`}
onMouseEnter={() => {
set_global_count((c) => c + 1);
set_departure_count((c) => c + 1);
}}
onMouseLeave={() => {
set_global_count((c) => c - 1);
set_departure_count((c) => c - 1);
}}
onClick={toggle_departure}
>
Ida
</span>
<span
class={`text-center border-2 border-transparent ${return_classes()}`}
onMouseEnter={() => {
set_global_count((c) => c + 1);
set_return_count((c) => c + 1);
}}
onMouseLeave={() => {
set_global_count((c) => c - 1);
set_return_count((c) => c - 1);
}}
onClick={toggle_return}
>
Vuelta
</span>
</button>
);
}

154
src/pages/IndexMobile.tsx Normal file
View File

@ -0,0 +1,154 @@
import { createEffect, createResource, createSignal, For, onMount } from "solid-js";
import L from "leaflet";
import { Line, Lines, Route, Routes } from "../new_types";
let global_map: L.Map | null = null;
const [global_count, set_global_count] = createSignal(0);
export function IndexMobile() {
const container = <div class="h-screen w-screen md:rounded-lg dark:opacity-80" />;
onMount(() => {
// Detect screen size and ratio,
// and redirect to pc page if neccesary
if (!window.matchMedia("only screen and (max-width: 760px)").matches) {
window.location.replace("/#/");
return;
}
global_map = L.map(container as HTMLElement)
.setView([-16.40171, -71.53040], 13);
L.tileLayer("/tiles/{z}/{x}/{y}.jpg", {
maxZoom: 17,
minZoom: 12,
attribution: "Map data &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
}).addTo(global_map);
});
return (
<div>
{container}
<LinesContainer />
</div>
);
}
function LinesContainer() {
const [selected_line, set_selected_line] = createSignal(-1);
const [linesData] = createResource<Lines>(async() => {
const res = await fetch("/n/lines.json");
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
return (
<div class="fixed bottom-0 left-0 w-screen text-c-on-bg z-[500]">
<div class="py-1">
<For each={linesData() ?? []}>
{(line) => <RouteChipContainer line={line} active_line={selected_line()} />}
</For>
</div>
<div class="py-2 bg-c-bg">
<For each={linesData() ?? []}>
{(line) => (
<LineChip
line={line}
activate={() => set_selected_line(line.id)}
deactivate={() => set_selected_line(-1)}
selected={selected_line() === line.id}
/>
)}
</For>
</div>
</div>
);
}
function LineChip(props: {
line: Line,
selected: boolean,
activate: () => void,
deactivate: () => void,
}) {
const active_classes = () => (props.selected ? "bg-r-hover-bg text-r-hover-on-bg" : "text-r-hover-bg");
return (
<button
class={`inline-block border-2 border-r-hover-bg py-1 px-4 rounded-lg ml-2 font-bold ${active_classes()}`}
style={`--hover-bg: ${props.line.hover_bg};--hover-on-bg: ${props.line.hover_on_bg}`}
onClick={() => {
if (props.selected) {
props.deactivate();
} else {
props.activate();
}
}}
>
Linea {props.line.name}
</button>
);
}
function RouteChipContainer(props: {line: Line, active_line: number}) {
const [routesData] = createResource<Routes>(async() => {
const res = await fetch(`/n/routes_${props.line.id}.json`);
if (!res.ok) throw new Error("Error fetching data");
return res.json();
});
const container_classes = () => (props.active_line === props.line.id ? "inline-block" : "hidden");
return (
<div class={container_classes()}
style={`--hover-bg: ${props.line.hover_bg};--hover-on-bg: ${props.line.hover_on_bg}`}
>
<For each={routesData() ?? []}>
{(route) => <RouteChip route={route} color={props.line.color} />}
</For>
</div>
);
}
function RouteChip(props: {route: Route, color: string}) {
const route = props.route;
const [active, set_active] = createSignal(false);
// Create departure and return polylines
const departure_polyline = L.polyline(route.departure, { color: props.color });
const return_polyline = L.polyline(route.return, { color: props.color });
// Render the lines into the map
// TODO: Change the UI to allow selecting departure and return separately
// and then update this
createEffect(() => {
if (global_count() === 0 || active() === true) {
departure_polyline.addTo(global_map!);
return_polyline.addTo(global_map!);
} else {
departure_polyline.removeFrom(global_map!);
return_polyline.removeFrom(global_map!);
}
});
const toggle_active = () => {
if (active()) {
set_active(false);
set_global_count((c) => c - 1);
} else {
set_active(true);
set_global_count((c) => c + 1);
}
};
const active_classes = () => (active() === true ? "bg-r-hover-bg text-r-hover-on-bg" : "bg-white text-r-hover-bg");
return (
<button
class={`inline-block rounded-full ml-2 border border-r-hover-bg py-1 px-2
${active_classes()}`}
onClick={toggle_active}
>
Ruta {route.name}
</button>
);
}