diff options
| -rw-r--r-- | .tmuxinator.yml | 4 | ||||
| -rwxr-xr-x | backend/bin/dev-server | 2 | ||||
| -rw-r--r-- | backend/build.zig | 26 | ||||
| -rw-r--r-- | backend/build.zig.zon | 14 | ||||
| -rw-r--r-- | backend/src/repos/maps_repo.zig | 4 | ||||
| -rw-r--r-- | backend/src/repos/markers_repo.zig | 4 | ||||
| -rw-r--r-- | backend/src/repos/users_repo.zig | 4 | ||||
| -rw-r--r-- | flake.lock | 12 | ||||
| -rw-r--r-- | flake.nix | 2 | ||||
| -rw-r--r-- | frontend/styles/pages/map.sass | 7 | ||||
| -rw-r--r-- | frontend/ts/src/lib/leaflet.d.ts | 6 | ||||
| -rw-r--r-- | frontend/ts/src/lib/rx.ts | 18 | ||||
| -rw-r--r-- | frontend/ts/src/pages/map.ts | 74 | ||||
| -rw-r--r-- | frontend/ts/src/pages/map/footer.ts | 25 | ||||
| -rw-r--r-- | frontend/ts/src/pages/map/marker.ts | 20 |
15 files changed, 151 insertions, 71 deletions
diff --git a/.tmuxinator.yml b/.tmuxinator.yml index 83fe649..7ef9740 100644 --- a/.tmuxinator.yml +++ b/.tmuxinator.yml @@ -11,9 +11,9 @@ windows: - sleep 1 - nix develop - cd backend - - bin/dev-server maps-backend + - bin/dev-server - frontend: - sleep 2 - nix develop - cd frontend - - bin/dev-server maps-frontend + - bin/dev-server diff --git a/backend/bin/dev-server b/backend/bin/dev-server index 680e736..0952209 100755 --- a/backend/bin/dev-server +++ b/backend/bin/dev-server @@ -14,7 +14,7 @@ watchexec \ --watch src \ --clear clear \ --restart \ - zig build run & + zig build -Dtarget=x86_64-linux run & LIVE_SERVER_PID="$!" while true; do sleep 1; done diff --git a/backend/build.zig b/backend/build.zig index 3fd7c48..fc26d37 100644 --- a/backend/build.zig +++ b/backend/build.zig @@ -1,17 +1,21 @@ const std = @import("std"); pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); + const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "backend", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), }); + b.installArtifact(exe); + const httpz = b.dependency("httpz", .{ .target = target, .optimize = optimize, @@ -22,12 +26,10 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - exe.linkLibC(); - exe.linkSystemLibrary("sqlite3"); + // exe.linkLibC(); + // exe.linkSystemLibrary("sqlite3"); exe.root_module.addImport("zqlite", zqlite.module("zqlite")); - b.installArtifact(exe); - const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { @@ -38,9 +40,11 @@ pub fn build(b: *std.Build) void { run_step.dependOn(&run_cmd.step); const exe_unit_tests = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), }); const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); diff --git a/backend/build.zig.zon b/backend/build.zig.zon index 11fd851..cd8301e 100644 --- a/backend/build.zig.zon +++ b/backend/build.zig.zon @@ -1,11 +1,11 @@ -.{ .name = .backend, .version = "0.0.0", .fingerprint = 0x8fe5c971579344fd, .minimum_zig_version = "0.14.0", .dependencies = .{ - .httpz = .{ - .url = "git+https://github.com/karlseguin/http.zig?ref=master#8ecf3a330ab1bed8495604d444e549b94f08bc0f", - .hash = "httpz-0.0.0-PNVzrOy2BgBA2lU1zixuVrv0UUkSnVrBUIlIHl1XV0XV", - }, +.{ .name = .backend, .version = "0.0.0", .fingerprint = 0x8fe5c971579344fd, .minimum_zig_version = "0.15.1", .dependencies = .{ .zqlite = .{ - .url = "git+https://github.com/karlseguin/zqlite.zig?ref=master#3cd6edca7d735fe9a8545c699091cfcebe1feb7f", - .hash = "zqlite-0.0.0-RWLaY0Z7lQApOOwYrJMyZomy1GlA3vRnmtrTaWQ2BSQK", + .url = "git+https://github.com/karlseguin/zqlite.zig?ref=master#74e661633ffe04fadc214ad07857633fa339f79c", + .hash = "zqlite-0.0.0-RWLaY81-lQAbR_uUQJeKmTzYi16C9Zvl-BjrBOavQfeK", + }, + .httpz = .{ + .url = "git+https://github.com/karlseguin/http.zig?ref=master#90e7fc07ef7157a4da6922571fb4daef38b48998", + .hash = "httpz-0.0.0-PNVzrHLABgDoVnzqKpEbx2rZIzmMclEdoEmdJDfZmxsv", }, }, .paths = .{ "build.zig", diff --git a/backend/src/repos/maps_repo.zig b/backend/src/repos/maps_repo.zig index 8031827..8e38877 100644 --- a/backend/src/repos/maps_repo.zig +++ b/backend/src/repos/maps_repo.zig @@ -16,9 +16,9 @@ pub fn get_maps(allocator: std.mem.Allocator, conn: zqlite.Conn) !std.ArrayList( var rows = try conn.rows(query, .{}); defer rows.deinit(); - var list = std.ArrayList(Map).init(allocator); + var list: std.ArrayList(Map) = .{}; while (rows.next()) |row| { - try list.append(Map{ + try list.append(allocator, Map{ .id = try allocator.dupe(u8, row.text(0)), .name = try allocator.dupe(u8, row.text(1)), }); diff --git a/backend/src/repos/markers_repo.zig b/backend/src/repos/markers_repo.zig index 232b8b6..9fc2b15 100644 --- a/backend/src/repos/markers_repo.zig +++ b/backend/src/repos/markers_repo.zig @@ -24,9 +24,9 @@ pub fn get_markers(allocator: std.mem.Allocator, conn: zqlite.Conn, map_id: []co defer rows.deinit(); - var list = std.ArrayList(Marker).init(allocator); + var list: std.ArrayList(Marker) = .{}; while (rows.next()) |row| { - try list.append(Marker{ + try list.append(allocator, Marker{ .id = try allocator.dupe(u8, row.text(0)), .lat = row.float(1), .lng = row.float(2), diff --git a/backend/src/repos/users_repo.zig b/backend/src/repos/users_repo.zig index 0f4ea82..9bde514 100644 --- a/backend/src/repos/users_repo.zig +++ b/backend/src/repos/users_repo.zig @@ -39,7 +39,7 @@ pub fn check_password(allocator: std.mem.Allocator, conn: zqlite.Conn, email: [] defer row.deinit(); const hash = row.text(0); const verify_options = bcrypt.VerifyOptions{ .silently_truncate_password = false }; - std.time.sleep(100000000 + rand.intRangeAtMost(u32, 0, 100000000)); + std.Thread.sleep(100000000 + rand.intRangeAtMost(u32, 0, 100000000)); bcrypt.strVerify(hash, password, verify_options) catch { return null; }; @@ -48,7 +48,7 @@ pub fn check_password(allocator: std.mem.Allocator, conn: zqlite.Conn, email: [] .name = try allocator.dupe(u8, row.text(2)), }; } else { - std.time.sleep(500000000 + rand.intRangeAtMost(u32, 0, 500000000)); + std.Thread.sleep(500000000 + rand.intRangeAtMost(u32, 0, 500000000)); return null; } } @@ -36,11 +36,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1746276987, - "narHash": "sha256-dMBHCH4wH2VzoPowkiRTljpiC/ihZ6Q0fdDH+K0Y9fQ=", + "lastModified": 1756565854, + "narHash": "sha256-65gr4nJOsp5W4Q27XS8SgaY90TtCe8GuNbXLVtID33s=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2490f37321fed41b50849efe74013903a6c320b5", + "rev": "8bb4e10098f4f5e47730856e8de5f8836ff7d49a", "type": "github" }, "original": { @@ -82,11 +82,11 @@ ] }, "locked": { - "lastModified": 1746232344, - "narHash": "sha256-t4dwFdoEksxY8vAX/95ybPSJV7lxZQr7kpmRNv6c69I=", + "lastModified": 1756555914, + "narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "08e5c921bdf1aff9772874425e09b2a69a972584", + "rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6", "type": "github" }, "original": { @@ -36,7 +36,7 @@ # Backend psmisc # fuser sqlite - zigpkgs."0.14.0" + zigpkgs."0.15.1" # Tooling tmux diff --git a/frontend/styles/pages/map.sass b/frontend/styles/pages/map.sass index 53c5dc4..1ac6c27 100644 --- a/frontend/styles/pages/map.sass +++ b/frontend/styles/pages/map.sass @@ -26,8 +26,15 @@ &__FooterButtons display: flex + align-items: center gap: 1rem + .g-ReadOnly + &--Active + background-color: colors.$green + &:not(.g-ReadOnly--Active) + opacity: 0.2 + .g-ContextMenu z-index: 1000 position: absolute diff --git a/frontend/ts/src/lib/leaflet.d.ts b/frontend/ts/src/lib/leaflet.d.ts index 76b88fd..ab8b038 100644 --- a/frontend/ts/src/lib/leaflet.d.ts +++ b/frontend/ts/src/lib/leaflet.d.ts @@ -40,6 +40,12 @@ export interface Layer { addEventListener: (name: string, fn: (e: MapEvent) => void) => void getLatLng: () => Pos setLatLng: (pos: Pos) => void + dragging: Dragging +} + +export interface Dragging { + enable(): () => void + disable(): () => void } export function tileLayer(url: string): Layer diff --git a/frontend/ts/src/lib/rx.ts b/frontend/ts/src/lib/rx.ts index 5edd3c1..b532842 100644 --- a/frontend/ts/src/lib/rx.ts +++ b/frontend/ts/src/lib/rx.ts @@ -1,4 +1,4 @@ -// Rx 3.0.0 +// Rx 3.1.0 // Html @@ -266,16 +266,22 @@ export function pure<A>(value: A): Rx<A> { return new Pure(value) } -class Var<A> extends Rx<A> { +export class Var<A> extends Rx<A> { readonly type: 'Var' readonly id: string readonly update: (f: (value: A) => A) => void + readonly now: () => A - constructor(id: string, update: (v: Var<A>) => ((f: ((value: A) => A)) => void)) { + constructor( + id: string, + update: (v: Var<A>) => ((f: ((value: A) => A)) => void), + now: (v: Var<A>) => (() => A) + ) { super() this.id = id this.type = 'Var' this.update = update(this) + this.now = now(this) } } @@ -350,7 +356,11 @@ class State { } register<A>(initValue: A) { - const v = new Var(this.varCounter.toString(), v => (f => this.update(v, f))) + const v = new Var( + this.varCounter.toString(), + v => (f => this.update(v, f)), + v => () => this.get(v) + ) this.varCounter += BigInt(1) this.state[v.id] = { value: initValue, diff --git a/frontend/ts/src/pages/map.ts b/frontend/ts/src/pages/map.ts index b445f63..42da69c 100644 --- a/frontend/ts/src/pages/map.ts +++ b/frontend/ts/src/pages/map.ts @@ -1,4 +1,4 @@ -import { h, Html } from 'lib/rx' +import { h, Html, Var } from 'lib/rx' import * as rx from 'lib/rx' import * as request from 'request' import * as modal from 'ui/modal' @@ -13,9 +13,9 @@ import * as L from 'lib/loadable' import * as layout from 'ui/layout' export function view(id: string): Html { - return rx.withState2<L.Loadable<Map>, L.Loadable<markerModel.Map>>( - [L.loading, L.loading], - (mapVar, markersVar) => { + return rx.withState3<L.Loadable<Map>, L.Loadable<markerModel.Map>, boolean>( + [L.loading, L.loading, false], + (mapVar, markersVar, readOnlyVar) => { request .get<Map>(`/api/maps/${id}`) .then(res => mapVar.update(_ => L.loaded(res))) @@ -23,7 +23,11 @@ export function view(id: string): Html { request .get<Array<markerModel.Marker>>(`/api/markers?map=${id}`) - .then(res => markersVar.update(_ => L.loaded(markerModel.toMap(res)))) + .then(res => { + markersVar.update(_ => L.loaded(markerModel.toMap(res))) + // Set to readonly if there is one marker + if (res.length > 0) readOnlyVar.update(_ => true) + }) .catch(({ message }) => markersVar.update(_ => L.failure(message))) return rx.map2( @@ -33,8 +37,14 @@ export function view(id: string): Html { L.map2([map, markers], (map, markers) => ({ map, markers })), ({map, markers}) => h('div', { className: 'g-Map' }, - withMap(map => mapView(id, map, markers)), - footer.view(map) + withMap(leafletMap => + [ mapView(id, leafletMap, markers, readOnlyVar), + footer.view({ + mapInit: map, + readOnly: readOnlyVar, + onUpdateReadOnly: (b: boolean) => readOnlyVar.update(_ => b) + }) ] + ) ) ) } @@ -74,7 +84,7 @@ interface ErrorModal { message: string } -function mapView(map_id: string, map: M.Map, markers: markerModel.Map): Html { +function mapView(map_id: string, map: M.Map, markers: markerModel.Map, readOnlyVar: Var<boolean>): Html { let lastUserAdded: markerModel.Marker | undefined return rx.withState3< @@ -84,9 +94,17 @@ function mapView(map_id: string, map: M.Map, markers: markerModel.Map): Html { >( [undefined, undefined, undefined], (addModalVar, updateModalVar, errorModalVar) => { - map.addEventListener('click', (event: M.MapEvent) => addModalVar.update(_ => event.latlng)) + map.addEventListener('click', (event: M.MapEvent) => { + if (!readOnlyVar.now()) { + addModalVar.update(_ => event.latlng) + } + }) - const onClick = (m: MarkerContext) => updateModalVar.update(_ => m) + const onClick = (m: MarkerContext) => { + if (!readOnlyVar.now()) { + updateModalVar.update(_ => m) + } + } const onMoveError = (message: string) => { errorModalVar.update(_ => ({ @@ -97,7 +115,7 @@ function mapView(map_id: string, map: M.Map, markers: markerModel.Map): Html { const elements = M.featureGroup() Object.values(markers).forEach(marker => { - const elem = addMarker({ marker, markers, map, onClick, onMoveError }) + const elem = addMarker({ marker, markers, map, onClick, onMoveError, readOnlyVar }) elements.addLayer(elem) }) @@ -120,7 +138,7 @@ function mapView(map_id: string, map: M.Map, markers: markerModel.Map): Html { body ), onSuccess: marker => { - addMarker({ marker, markers, map, onClick, onMoveError }) + addMarker({ marker, markers, map, onClick, onMoveError, readOnlyVar }) addModalVar.update(_ => undefined) markers[marker.id] = marker lastUserAdded = marker @@ -145,7 +163,7 @@ function mapView(map_id: string, map: M.Map, markers: markerModel.Map): Html { ), onSuccess: marker => { map.removeLayer(markerElem) - addMarker({ marker, markers, map, onClick, onMoveError }) + addMarker({ marker, markers, map, onClick, onMoveError, readOnlyVar }) updateModalVar.update(_ => undefined) markers[marker.id] = marker lastUserAdded = marker @@ -175,24 +193,30 @@ interface AddMarkerParams { map: M.Map onClick: (m: MarkerContext) => void onMoveError: (message: string) => void + readOnlyVar: Var<boolean> } -function addMarker({ markers, marker, map, onClick, onMoveError }: AddMarkerParams): M.FeatureGroup { +function addMarker({ markers, marker, map, onClick, onMoveError, readOnlyVar }: AddMarkerParams): M.FeatureGroup { const elem = markerView.create({ marker, onMove: markerElem => { - const pos = markerElem.getLatLng() - const newMarker = structuredClone(marker) - newMarker.lat = pos.lat - newMarker.lng = pos.lng - updateMarker(newMarker) - .then(m => markers[marker.id] = m) - .catch(({ message }) => { - markerElem.setLatLng({ lat: marker.lat, lng: marker.lng }) - onMoveError(message) - }) + if (!readOnlyVar.now()) { + const pos = markerElem.getLatLng() + const newMarker = structuredClone(marker) + newMarker.lat = pos.lat + newMarker.lng = pos.lng + updateMarker(newMarker) + .then(m => markers[marker.id] = m) + .catch(({ message }) => { + markerElem.setLatLng({ lat: marker.lat, lng: marker.lng }) + onMoveError(message) + }) + } else { + markerElem.setLatLng({ lat: marker.lat, lng: marker.lng }) + } }, - onClick: (markerElem: M.FeatureGroup) => onClick({ markerId: marker.id, markerElem }) + onClick: (markerElem: M.FeatureGroup) => onClick({ markerId: marker.id, markerElem }), + readOnlyVar }) map.addLayer(elem) diff --git a/frontend/ts/src/pages/map/footer.ts b/frontend/ts/src/pages/map/footer.ts index 945bfd4..f514c66 100644 --- a/frontend/ts/src/pages/map/footer.ts +++ b/frontend/ts/src/pages/map/footer.ts @@ -1,4 +1,4 @@ -import { h, Html } from 'lib/rx' +import { h, Html, Rx } from 'lib/rx' import * as rx from 'lib/rx' import * as request from 'request' import * as modal from 'ui/modal' @@ -7,12 +7,22 @@ import { Map } from 'models/map' import * as form from 'lib/form' import * as L from 'lib/loadable' -export function view(mapInit: Map): Html { +interface ViewParams { + mapInit: Map, + readOnly: Rx<boolean>, + onUpdateReadOnly: (b: boolean) => void, +} + +export function view({ mapInit, readOnly, onUpdateReadOnly }: ViewParams): Html { return h('footer', { className: 'g-Map__Footer' }, rx.withState<Map>(mapInit, mapVar => mapVar.map(map => [ - map.name, + h('div', + { className: 'g-Map__FooterButtons' }, + map.name, + readOnlyButton(readOnly, onUpdateReadOnly) + ), h('div', { className: 'g-Map__FooterButtons' }, viewRenameButton(map, (map: Map) => mapVar.update(_ => map)), @@ -23,6 +33,15 @@ export function view(mapInit: Map): Html { ) } +function readOnlyButton(readOnly: Rx<boolean>, onUpdateReadOnly: (b: boolean) => void): Html { + return h('button', + { className: readOnly.map(b => `g-Button g-ReadOnly ${b ? 'g-ReadOnly--Active' : ''}`), + onclick: readOnly.map(b => (event: Event) => onUpdateReadOnly(!b)) + }, + 'Lecture seule' + ) +} + // Rename modal function viewRenameButton(map: Map, onUpdate: (m: Map) => void): Html { diff --git a/frontend/ts/src/pages/map/marker.ts b/frontend/ts/src/pages/map/marker.ts index b690741..435ed5b 100644 --- a/frontend/ts/src/pages/map/marker.ts +++ b/frontend/ts/src/pages/map/marker.ts @@ -1,16 +1,17 @@ -import { mount, h, s } from 'lib/rx' +import { Var, mount, h, s } from 'lib/rx' import * as Color from 'lib/color' import * as M from 'lib/leaflet' import * as icons from 'lib/icons' import * as markerModel from 'models/marker' interface CreateParams { - marker: markerModel.Marker, - onMove: (marker: M.Layer) => void, - onClick: (markerElem: M.FeatureGroup) => void, + marker: markerModel.Marker + onMove: (marker: M.Layer) => void + onClick: (markerElem: M.FeatureGroup) => void + readOnlyVar: Var<boolean> } -export function create({ marker, onMove, onClick }: CreateParams): M.FeatureGroup { +export function create({ marker, onMove, onClick, readOnlyVar }: CreateParams): M.FeatureGroup { const { lat, lng, color, icon, name, description, radius } = marker const pos = { lat, lng } @@ -30,6 +31,15 @@ export function create({ marker, onMove, onClick }: CreateParams): M.FeatureGrou ? M.featureGroup([ markerElem, circle ]) : M.featureGroup([ markerElem ]) + // Fired before dragging, permits to disable dragging just at the right + // moment if in readonly mode. + markerElem.addEventListener('mousedown', () => { + if (readOnlyVar.now()) { + markerElem.dragging.disable() + window.setTimeout(() => markerElem.dragging.enable()) + } + }) + markerElem.addEventListener('drag', e => { circle && circle.setLatLng(markerElem.getLatLng()) }) |
