completed CRUD operations
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -70,6 +70,7 @@ public abstract class BaseImporter implements Importer {
|
||||
return Error.of("not implemented");
|
||||
}
|
||||
|
||||
|
||||
protected Result<LocalDateTime> extractEnd(Tag eventTag) {
|
||||
Result<Tag> endTag = extractEndTag(eventTag);
|
||||
if (endTag.optional().isEmpty()) return transform(endTag);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.cal;
|
||||
|
||||
import de.srsoftware.cal.api.Coords;
|
||||
import de.srsoftware.tools.Error;
|
||||
import de.srsoftware.tools.Payload;
|
||||
import de.srsoftware.tools.Result;
|
||||
|
||||
public class Util {
|
||||
public static Result<Coords> extractCoords(String coords) {
|
||||
if (coords == null) return de.srsoftware.tools.Error.of("Argument is null");
|
||||
if (coords.isBlank()) return de.srsoftware.tools.Error.of("Argument is blank");
|
||||
var parts = coords.split(",");
|
||||
if (parts.length != 2) return de.srsoftware.tools.Error.format("Argument has invalid format: %s", coords);
|
||||
try {
|
||||
var lat = Double.parseDouble(parts[0].trim());
|
||||
var lon = Double.parseDouble(parts[1].trim());
|
||||
return Payload.of(new Coords(lon, lat));
|
||||
} catch (NumberFormatException nfe) {
|
||||
return Error.of("Failed to parse coords from %s".formatted(coords), nfe);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ dependencies {
|
||||
implementation(project(":de.srsoftware.cal.api"))
|
||||
implementation(project(":de.srsoftware.cal.base"))
|
||||
|
||||
implementation("de.srsoftware:tools.jdbc:1.1.2")
|
||||
implementation("de.srsoftware:tools.jdbc:1.1.3")
|
||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||
implementation("de.srsoftware:tools.util:1.2.3")
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
package de.srsoftware.cal.db;
|
||||
|
||||
import de.srsoftware.cal.api.Appointment;
|
||||
import de.srsoftware.tools.Error;
|
||||
import de.srsoftware.tools.Result;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -19,6 +22,8 @@ public interface Database {
|
||||
*/
|
||||
public Result<Appointment> add(Appointment appointment);
|
||||
|
||||
Result<List<String>> findTags(String infix);
|
||||
|
||||
/**
|
||||
* list appointments unfiltered
|
||||
* @param count the maximum number of appointments to return
|
||||
@@ -50,7 +55,7 @@ public interface Database {
|
||||
|
||||
Result<Appointment> loadEvent(String location, LocalDateTime start);
|
||||
|
||||
Result<List<String>> findTags(String infix);
|
||||
Result<Long> removeAppointment(long id);
|
||||
|
||||
Result<Appointment> update(Appointment baseAppointment);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import static de.srsoftware.tools.jdbc.Query.*;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
|
||||
import de.srsoftware.cal.BaseAppointment;
|
||||
import de.srsoftware.cal.Util;
|
||||
import de.srsoftware.cal.api.Appointment;
|
||||
import de.srsoftware.cal.api.Attachment;
|
||||
import de.srsoftware.cal.api.Link;
|
||||
@@ -251,17 +252,25 @@ public class MariaDB implements Database {
|
||||
|
||||
private Result<BaseAppointment> createAppointmentOf(ResultSet results) throws SQLException {
|
||||
var id = results.getInt(AID);
|
||||
var title = results.getString("title");
|
||||
var description = results.getString("description");
|
||||
var title = results.getString(TITLE);
|
||||
var description = results.getString(DESCRIPTION);
|
||||
if (allEmpty(title, description)) return Error.format("Title and Description of appointment %s are empty", id);
|
||||
var start = results.getTimestamp("start").toLocalDateTime();
|
||||
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null);
|
||||
var location = results.getString("location");
|
||||
var start = results.getTimestamp(START).toLocalDateTime();
|
||||
var end = nullable(results.getTimestamp(END)).map(Timestamp::toLocalDateTime).orElse(null);
|
||||
var location = results.getString(LOCATION);
|
||||
var appointment = new BaseAppointment(id, title, description, start, end, location);
|
||||
|
||||
try {
|
||||
Util.extractCoords(results.getString(COORDS)).optional().ifPresent(appointment::coords);
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING, "Failed to read coordinates from database!");
|
||||
}
|
||||
|
||||
try {
|
||||
var tags = nullIfEmpty(results.getString("tags"));
|
||||
if (tags != null) appointment.tags(tags.split(","));
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING, "Failed to read tags from database!");
|
||||
}
|
||||
return Payload.of(appointment);
|
||||
}
|
||||
@@ -290,6 +299,19 @@ public class MariaDB implements Database {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Long> removeAppointment(long id) {
|
||||
try {
|
||||
Query.delete().from(APPOINTMENTS).where(AID,equal(id)).execute(connection);
|
||||
Query.delete().from(APPOINTMENT_TAGS).where(AID,equal(id)).execute(connection);
|
||||
Query.delete().from(APPOINTMENT_ATTACHMENTS).where(AID,equal(id)).execute(connection);
|
||||
Query.delete().from(APPOINTMENT_URLS).where(AID,equal(id)).execute(connection);
|
||||
return Payload.of(id);
|
||||
} catch (SQLException e) {
|
||||
return Error.of("Failed to delete event %s".formatted(id),e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Appointment> update(Appointment event) {
|
||||
var end = event.end().map(Timestamp::valueOf).orElse(null);
|
||||
|
||||
@@ -11,10 +11,8 @@ import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.cal.api.Appointment;
|
||||
import de.srsoftware.cal.api.Link;
|
||||
import de.srsoftware.cal.db.Database;
|
||||
import de.srsoftware.tools.*;
|
||||
import de.srsoftware.tools.Error;
|
||||
import de.srsoftware.tools.PathHandler;
|
||||
import de.srsoftware.tools.Payload;
|
||||
import de.srsoftware.tools.Result;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
@@ -22,6 +20,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class ApiHandler extends PathHandler {
|
||||
@@ -44,12 +43,42 @@ public class ApiHandler extends PathHandler {
|
||||
case "/tags" -> listTags(ex,params);
|
||||
default -> PathHandler.notFound(ex);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||
public boolean doDelete(String path, HttpExchange ex) throws IOException {
|
||||
var params = queryParam(ex);
|
||||
return switch (path) {
|
||||
case "/event" -> delete(ex,params);
|
||||
default -> PathHandler.notFound(ex);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean delete(HttpExchange ex, Map<String,String> params) throws IOException {
|
||||
var aid = params.get(AID);
|
||||
if (aid == null) return sendContent(ex,Error.of("Missing appointment id"));
|
||||
long id = 0;
|
||||
try {
|
||||
id = Long.parseLong(aid);
|
||||
} catch (Exception e){
|
||||
return sendContent(ex,Error.format("%s is not a valid id!",aid));
|
||||
}
|
||||
var json = json(ex);
|
||||
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) :
|
||||
null;
|
||||
if (title == null) return sendContent(ex, Error.of("Missing appointment title"));
|
||||
var res = db.loadEvent(id);
|
||||
if (res.optional().isEmpty()) return sendContent(ex, res);
|
||||
var event = res.optional().get();
|
||||
if (!title.equals(event.title())) return sendContent(ex, Error.of("Title mismatch!"));
|
||||
var del = db.removeAppointment(id);
|
||||
if (del.optional().isEmpty()) return sendContent(ex,del);
|
||||
return sendContent(ex,Map.of("deleted",del.optional().get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||
return switch (path) {
|
||||
case "/event/edit" -> editEvent(ex);
|
||||
default -> PathHandler.notFound(ex);
|
||||
};
|
||||
@@ -58,129 +87,135 @@ public class ApiHandler extends PathHandler {
|
||||
// spotless:off
|
||||
private boolean editEvent(HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
||||
var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null;
|
||||
if (allSet(location, start)) {
|
||||
var existingAppointment = db.loadEvent(location, start).optional();
|
||||
if (existingAppointment.isPresent()) {
|
||||
var event = existingAppointment.get();
|
||||
json.put(AID,event.id());
|
||||
return update(ex,toEvent(json));
|
||||
Optional<Appointment> existingAppointment = Optional.empty();
|
||||
Long aid = json.has(AID) ? json.getLong(AID) : null;
|
||||
if (aid != null) { // load appointment by aid
|
||||
existingAppointment = db.loadEvent(aid).optional();
|
||||
} else { // try to load appointment by location @ time
|
||||
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
||||
var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null;
|
||||
if (allSet(location, start)) {
|
||||
existingAppointment = db.loadEvent(location, start).optional();
|
||||
}
|
||||
}
|
||||
if (existingAppointment.isPresent()) {
|
||||
var event = existingAppointment.get();
|
||||
json.put(AID,event.id());
|
||||
return update(ex,toEvent(json));
|
||||
}
|
||||
return createEvent(ex, json);
|
||||
}
|
||||
|
||||
|
||||
// spotless:on
|
||||
|
||||
private Result<BaseAppointment> toEvent(JSONObject json) {
|
||||
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
|
||||
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
|
||||
if (title == null) return Error.of("title missing");
|
||||
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
|
||||
if (start == null) return Error.of("start missing");
|
||||
var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
||||
var end = json.has(END) ? nullIfEmpty(json.getString(END)) : null;
|
||||
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
||||
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
||||
if (location == null) return Error.of("location missing");
|
||||
var aid = json.has(AID) ? json.getLong(AID) : 0;
|
||||
var event = new BaseAppointment(aid, title, description, startDate, endDate, location);
|
||||
if (json.has(ATTACHMENTS)) {
|
||||
json.getJSONArray(ATTACHMENTS).forEach(att -> {
|
||||
Payload //
|
||||
.of(att.toString())
|
||||
.map(BaseImporter::url)
|
||||
.map(BaseImporter::toAttachment)
|
||||
.optional()
|
||||
.ifPresent(event::add);
|
||||
});
|
||||
}
|
||||
if (json.has(LINKS)) {
|
||||
json.getJSONArray(LINKS).forEach(o -> {
|
||||
if (o instanceof JSONObject j) toLink(j).optional().ifPresent(event::addLinks);
|
||||
});
|
||||
}
|
||||
if (json.has(TAGS)) json.getJSONArray(TAGS).forEach(o -> event.tags(o.toString()));
|
||||
return Payload.of(event);
|
||||
}
|
||||
|
||||
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
|
||||
var eventRes = toEvent(json);
|
||||
if (eventRes.optional().isPresent()) {
|
||||
return sendContent(ex, db.add(eventRes.optional().get()).map(ApiHandler::toJson));
|
||||
}
|
||||
return sendContent(ex, eventRes);
|
||||
}
|
||||
|
||||
protected static Result<Link> toLink(JSONObject json) {
|
||||
try {
|
||||
var description = json.getString(DESCRIPTION);
|
||||
return Payload.of(json.getString(URL)).map(BaseImporter::url).map(url -> BaseImporter.link(url, description));
|
||||
|
||||
} catch (Exception e) {
|
||||
return Error.of("Failed to create link from %s".formatted(json), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean update(HttpExchange ex, Result<BaseAppointment> event) throws IOException {
|
||||
if (event.optional().isPresent()) return sendContent(ex, db.update(event.optional().get()).map(ApiHandler::toJson));
|
||||
return sendContent(ex, event);
|
||||
}
|
||||
|
||||
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||
var infix = params.get("infix");
|
||||
if (infix == null) return sendContent(ex, Error.of("No infix set in method call parameters"));
|
||||
var res = db.findTags(infix).map(ApiHandler::sortTags);
|
||||
return sendContent(ex, res);
|
||||
}
|
||||
|
||||
private static Result<List<String>> sortTags(Result<List<String>> listResult) {
|
||||
if (listResult.optional().isEmpty()) return listResult;
|
||||
List<String> list = listResult.optional().get();
|
||||
while (list.size() > 15) {
|
||||
int longest = list.stream().map(String::length).reduce(0, Integer::max);
|
||||
var subset = list.stream().filter(s -> s.length() < longest).toList();
|
||||
if (subset.size() < 3) return Payload.of(list);
|
||||
list = subset;
|
||||
}
|
||||
return Payload.of(list);
|
||||
}
|
||||
|
||||
private boolean listEvents(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
|
||||
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
|
||||
try {
|
||||
return PathHandler.sendContent(ex, db.list(start, end).stream().map(Appointment::json).toList());
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING, "Failed to fetch events (start = {0}, end = {1}!", start, end, e);
|
||||
}
|
||||
return PathHandler.notFound(ex);
|
||||
}
|
||||
|
||||
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||
var id = params.get("id");
|
||||
if (id != null) try {
|
||||
return PathHandler.sendContent(ex, db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson));
|
||||
} catch (NumberFormatException | IOException nfe) {
|
||||
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!", id));
|
||||
private Result<BaseAppointment> toEvent(JSONObject json) {
|
||||
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
|
||||
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
|
||||
if (title == null) return Error.of("title missing");
|
||||
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
|
||||
if (start == null) return Error.of("start missing");
|
||||
var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
||||
var end = json.has(END) ? nullIfEmpty(json.getString(END)) : null;
|
||||
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
||||
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
||||
if (location == null) return Error.of("location missing");
|
||||
var aid = json.has(AID) ? json.getLong(AID) : 0;
|
||||
var event = new BaseAppointment(aid, title, description, startDate, endDate, location);
|
||||
if (json.has(ATTACHMENTS)) {
|
||||
json.getJSONArray(ATTACHMENTS).forEach(att -> {
|
||||
Payload //
|
||||
.of(att.toString())
|
||||
.map(BaseImporter::url)
|
||||
.map(BaseImporter::toAttachment)
|
||||
.optional()
|
||||
.ifPresent(event::add);
|
||||
});
|
||||
}
|
||||
return PathHandler.sendContent(ex, Error.of("ID missing"));
|
||||
}
|
||||
if (json.has(LINKS)) {
|
||||
json.getJSONArray(LINKS).forEach(o -> {
|
||||
if (o instanceof JSONObject j) toLink(j).optional().ifPresent(event::addLinks);
|
||||
});
|
||||
}
|
||||
if (json.has(TAGS)) json.getJSONArray(TAGS).forEach(o -> event.tags(o.toString()));
|
||||
return Payload.of(event);
|
||||
}
|
||||
|
||||
private static Result<JSONObject> toJson(Result<Appointment> res) {
|
||||
var opt = res.optional();
|
||||
if (opt.isEmpty()) return Result.transform(res);
|
||||
return Payload.of(opt.get().json());
|
||||
}
|
||||
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
|
||||
var eventRes = toEvent(json);
|
||||
if (eventRes.optional().isPresent()) {
|
||||
return sendContent(ex, db.add(eventRes.optional().get()).map(ApiHandler::toJson));
|
||||
}
|
||||
return sendContent(ex, eventRes);
|
||||
}
|
||||
|
||||
protected static Result<Link> toLink(JSONObject json) {
|
||||
try {
|
||||
var description = json.getString(DESCRIPTION);
|
||||
return Payload.of(json.getString(URL)).map(BaseImporter::url).map(url -> BaseImporter.link(url, description));
|
||||
|
||||
} catch (Exception e) {
|
||||
return Error.of("Failed to create link from %s".formatted(json), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean update(HttpExchange ex, Result<BaseAppointment> event) throws IOException {
|
||||
if (event.optional().isPresent()) return sendContent(ex, db.update(event.optional().get()).map(ApiHandler::toJson));
|
||||
return sendContent(ex, event);
|
||||
}
|
||||
|
||||
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||
var infix = params.get("infix");
|
||||
if (infix == null) return sendContent(ex, Error.of("No infix set in method call parameters"));
|
||||
var res = db.findTags(infix).map(ApiHandler::sortTags);
|
||||
return sendContent(ex, res);
|
||||
}
|
||||
|
||||
private static Result<List<String>> sortTags(Result<List<String>> listResult) {
|
||||
if (listResult.optional().isEmpty()) return listResult;
|
||||
List<String> list = listResult.optional().get();
|
||||
while (list.size() > 15) {
|
||||
int longest = list.stream().map(String::length).reduce(0, Integer::max);
|
||||
var subset = list.stream().filter(s -> s.length() < longest).toList();
|
||||
if (subset.size() < 3) return Payload.of(list);
|
||||
list = subset;
|
||||
}
|
||||
return Payload.of(list);
|
||||
}
|
||||
|
||||
private boolean listEvents(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
|
||||
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
|
||||
try {
|
||||
return PathHandler.sendContent(ex, db.list(start, end).stream().map(Appointment::json).toList());
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING, "Failed to fetch events (start = {0}, end = {1}!", start, end, e);
|
||||
}
|
||||
return PathHandler.notFound(ex);
|
||||
}
|
||||
|
||||
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||
var id = params.get("id");
|
||||
if (id != null) try {
|
||||
return PathHandler.sendContent(ex, db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson));
|
||||
} catch (NumberFormatException | IOException nfe) {
|
||||
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!", id));
|
||||
}
|
||||
return PathHandler.sendContent(ex, Error.of("ID missing"));
|
||||
}
|
||||
|
||||
private static Result<JSONObject> toJson(Result<Appointment> res) {
|
||||
var opt = res.optional();
|
||||
if (opt.isEmpty()) return Result.transform(res);
|
||||
return Payload.of(opt.get().json());
|
||||
}
|
||||
|
||||
|
||||
private static LocalDateTime toLocalDateTime(String dateString) {
|
||||
try {
|
||||
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
private static LocalDateTime toLocalDateTime(String dateString) {
|
||||
try {
|
||||
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,13 @@
|
||||
<script src="/static/script/leaflet.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Create new event</h1>
|
||||
<button id="save" disabled>save</button>
|
||||
<h1>Create/Edit event</h1>
|
||||
<span id="buttons">
|
||||
<button id="save" disabled>save</button>
|
||||
<button id="cancel" onclick="location.href = location.href.replace('/static/edit','').replace('copy=','id=')">cancel</button>
|
||||
</span>
|
||||
<div class="form">
|
||||
<input type="hidden" id="aid" />
|
||||
<fieldset>
|
||||
<legend>basic data</legend>
|
||||
<table>
|
||||
@@ -113,5 +117,6 @@
|
||||
|
||||
|
||||
</div>
|
||||
<script>document.addEventListener("DOMContentLoaded", loadEvent);</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,20 +5,37 @@
|
||||
<link rel="stylesheet" href="occ.css" />
|
||||
<script src="/static/script/common.js"></script>
|
||||
<script src="/static/script/event.js"></script>
|
||||
<link rel="stylesheet" href="/static/leaflet.css" />
|
||||
<script src="/static/script/leaflet.js"></script>
|
||||
</head>
|
||||
<body class="event">
|
||||
<nav />
|
||||
<h1>Loading…</h1>
|
||||
<h1 id="title">Loading…</h1>
|
||||
<span id="buttons">
|
||||
<button title="create a copy of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit').replace('id=','copy=')">⧉ copy</button>
|
||||
<button title="edit the details of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit')">✍ edit</button>
|
||||
<button title="delete this event" onclick="confirmDelete()">🗑 delete</button>
|
||||
</span>
|
||||
<div id="time">Loading time…</div>
|
||||
<div id="description">Loading description…</div>
|
||||
<div id="links">Loading links…</div>
|
||||
<div id="attachments">Loading attachments…</div>
|
||||
<div id="tags">Loading tags…</div>
|
||||
<div id="map" style="width: 100%; height: 500px;"></div>
|
||||
<script>
|
||||
|
||||
var map = L.map('map').setView([51.160556, 10.4425], 9);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event){
|
||||
console.log("page loaded…");
|
||||
loadEventData();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,16 +27,14 @@
|
||||
|
||||
</th>
|
||||
<th>End</th>
|
||||
<th>Location</th>
|
||||
<th>Title</th>
|
||||
<th>Location</th>
|
||||
<th>Tags <span id="tag_selection"></span></th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event){
|
||||
loadCurrentEvents();
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", loadCurrentEvents);
|
||||
</script>
|
||||
<div id="overlay">
|
||||
|
||||
|
||||
@@ -48,14 +48,15 @@ body.event {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
background: chocolate;
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
border: 1px solid yellow;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#tag_selection {
|
||||
@@ -127,15 +128,22 @@ table#eventlist{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#save {
|
||||
min-width: 100px;
|
||||
min-height: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
#buttons {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
left: 50%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#cancel,
|
||||
#save {
|
||||
min-width: 80px;
|
||||
min-height: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -155,8 +155,10 @@ async function handleSave(response){
|
||||
if (response.ok){
|
||||
var json = await response.json();
|
||||
var url = window.location.href.replace('/static/edit','')
|
||||
if (json.id) url+='?highlight='+json.id;
|
||||
window.location.href = url; // TODO: add highlight on new event
|
||||
let pos = url.indexOf('?');
|
||||
if (pos>0) url = url.substring(0,pos);
|
||||
if (json.id) url+='?id='+json.id;
|
||||
window.location.href = url;
|
||||
} else {
|
||||
var json = await response.json();
|
||||
if (json.error) showError(json.error);
|
||||
@@ -169,6 +171,7 @@ async function saveEvent(){
|
||||
var location = element('location').value;
|
||||
var start = element('start').value;
|
||||
var event = {
|
||||
aid : element('aid').value,
|
||||
title : element('title').value,
|
||||
description : element('description').value,
|
||||
location : element('location').value,
|
||||
@@ -186,4 +189,53 @@ async function saveEvent(){
|
||||
'Content-Type' : 'appication/json'
|
||||
}
|
||||
}).then(handleSave);
|
||||
}
|
||||
|
||||
async function distributeEventData(response){
|
||||
if (response.ok){
|
||||
let event = await response.json();
|
||||
console.log({'event-data':event});
|
||||
var keys = ['title','location','description'];
|
||||
if (element('aid').value) {
|
||||
keys.push('start');
|
||||
keys.push('end');
|
||||
}
|
||||
for (var key of keys){
|
||||
if (event[key]) element(key).value = event[key];
|
||||
}
|
||||
if (event.coords){
|
||||
element('coords').value = event.coords.lat + ', ' + event.coords.lon;
|
||||
marker.setLatLng([event.coords.lat, event.coords.lon]);
|
||||
map.setView([event.coords.lat, event.coords.lon], 17);
|
||||
}
|
||||
|
||||
|
||||
var linkList = element('link-list');
|
||||
|
||||
for (var link of event.links){
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = `<a href="${link.url}">${link.description}</a><a onclick="removeParent(this)">❌</a> `;
|
||||
linkList.appendChild(span);
|
||||
}
|
||||
|
||||
var imgList = element('attachment-list');
|
||||
for (var image of event.attachments){
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = `<img src="${image.url}" /><a onclick="removeParent(this)">❌</a> `;
|
||||
imgList.appendChild(span);
|
||||
}
|
||||
|
||||
if (event.tags){
|
||||
for (var tag of event.tags) addTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadEvent(){
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
var aid = params.get('id');
|
||||
if (aid){
|
||||
element('aid').value = aid;
|
||||
} else aid = params.get('copy');
|
||||
if (aid) fetch('/api/event?id='+aid).then(distributeEventData);
|
||||
}
|
||||
@@ -17,17 +17,21 @@ function attachmentList(json){
|
||||
}
|
||||
|
||||
async function handleEventData(response){
|
||||
if (response.ok){
|
||||
var json = await response.json();
|
||||
document.getElementsByTagName('h1')[0].innerHTML = json.title ? json.title : '';
|
||||
document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : '');
|
||||
addDescription(json);
|
||||
document.getElementById('tags').innerHTML = "Tags: "+json.tags.join(" ");
|
||||
var links = document.getElementById('links');
|
||||
links.innerHTML = "";
|
||||
links.appendChild(linkList(json.links));
|
||||
attachmentList(json.attachments);
|
||||
}
|
||||
if (response.ok){
|
||||
var event = await response.json();
|
||||
document.getElementsByTagName('h1')[0].innerHTML = event.title ? event.title : '';
|
||||
document.getElementById('time').innerHTML = event.start + (event.end ? '…'+event.end : '');
|
||||
addDescription(event);
|
||||
document.getElementById('tags').innerHTML = "Tags: "+event.tags.join(" ");
|
||||
var links = document.getElementById('links');
|
||||
links.innerHTML = "";
|
||||
links.appendChild(linkList(event.links));
|
||||
attachmentList(event.attachments);
|
||||
if (event.coords){
|
||||
L.marker([event.coords.lat, event.coords.lon]).addTo(map);
|
||||
map.setView([event.coords.lat, event.coords.lon], 17);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function linkList(json){
|
||||
@@ -50,3 +54,28 @@ function loadEventData(){
|
||||
if (id) fetch('/api/event?id='+id).then(handleEventData);
|
||||
}
|
||||
|
||||
async function handleDeleted(response){
|
||||
if (response.ok){
|
||||
console.log(location.href);
|
||||
var url = location.href.replace('/static/event','/');
|
||||
console.log(url);
|
||||
var pos = url.indexOf('?');
|
||||
if (pos>0) url = url.substring(0,pos);
|
||||
console.log(url);
|
||||
window.top.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(){
|
||||
var title = element('title').innerHTML;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
var id = urlParams.get('id');
|
||||
if (confirm(`Do you really want to delete "${title}"?`)){
|
||||
fetch('/api/event?aid='+id,{
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
title: title
|
||||
})
|
||||
}).then(handleDeleted);
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ function addRow(json){
|
||||
addCell(row,json.id,json.id);
|
||||
addCell(row,json.start,json.id);
|
||||
addCell(row,json.end,json.id);
|
||||
addCell(row,json.location,json.id);
|
||||
addCell(row,json.title,json.id);
|
||||
addCell(row,json.location,json.id);
|
||||
row.appendChild(createTags(json.tags));
|
||||
}
|
||||
|
||||
@@ -80,15 +80,17 @@ async function handleEvents(response){
|
||||
updateTagVisibility();
|
||||
if (highlight){
|
||||
var row = element(highlight);
|
||||
row.classList.add('highlight');
|
||||
row.scrollIntoView({behavior: 'smooth',block: 'center'});
|
||||
if (row) {
|
||||
row.classList.add('highlight');
|
||||
row.scrollIntoView({behavior: 'smooth',block: 'center'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadCurrentEvents(){
|
||||
let params = new URLSearchParams(location.search);
|
||||
highlight = params.get('highlight');
|
||||
highlight = params.get('id');
|
||||
if (start == null){
|
||||
var now = new Date();
|
||||
var year = now.getFullYear();
|
||||
@@ -106,10 +108,11 @@ function showOverlay(url){
|
||||
div.appendChild(iframe);
|
||||
div.style.display = 'block';
|
||||
div.onclick = e => div.style.display = 'none';
|
||||
var btn = document.createElement('div');
|
||||
btn.id = 'close_overlay';
|
||||
btn.innerHTML = '✖';
|
||||
div.appendChild(btn);
|
||||
var closeBtn = document.createElement('div');
|
||||
closeBtn.id = 'close_overlay';
|
||||
closeBtn.innerHTML = '✖';
|
||||
closeBtn.title = 'close overlay';
|
||||
div.appendChild(closeBtn);
|
||||
}
|
||||
|
||||
function toggleTag(tag){
|
||||
|
||||
Reference in New Issue
Block a user