Browse Source

completed CRUD operations

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
main
Stephan Richter 9 months ago
parent
commit
c5ec3cc43b
  1. 1
      de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java
  2. 23
      de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/Util.java
  3. 2
      de.srsoftware.cal.db/build.gradle.kts
  4. 7
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java
  5. 32
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java
  6. 245
      de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiHandler.java
  7. 9
      de.srsoftware.cal.web/src/main/resources/edit.html
  8. 19
      de.srsoftware.cal.web/src/main/resources/event.html
  9. 6
      de.srsoftware.cal.web/src/main/resources/index.html
  10. 24
      de.srsoftware.cal.web/src/main/resources/occ.css
  11. 56
      de.srsoftware.cal.web/src/main/resources/script/edit.js
  12. 51
      de.srsoftware.cal.web/src/main/resources/script/event.js
  13. 19
      de.srsoftware.cal.web/src/main/resources/script/index.js

1
de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java

@ -70,6 +70,7 @@ public abstract class BaseImporter implements Importer {
return Error.of("not implemented"); return Error.of("not implemented");
} }
protected Result<LocalDateTime> extractEnd(Tag eventTag) { protected Result<LocalDateTime> extractEnd(Tag eventTag) {
Result<Tag> endTag = extractEndTag(eventTag); Result<Tag> endTag = extractEndTag(eventTag);
if (endTag.optional().isEmpty()) return transform(endTag); if (endTag.optional().isEmpty()) return transform(endTag);

23
de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/Util.java

@ -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);
}
}
}

2
de.srsoftware.cal.db/build.gradle.kts

@ -4,7 +4,7 @@ dependencies {
implementation(project(":de.srsoftware.cal.api")) implementation(project(":de.srsoftware.cal.api"))
implementation(project(":de.srsoftware.cal.base")) 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.optionals:1.0.0")
implementation("de.srsoftware:tools.util:1.2.3") implementation("de.srsoftware:tools.util:1.2.3")
} }

7
de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java

@ -2,10 +2,13 @@
package de.srsoftware.cal.db; package de.srsoftware.cal.db;
import de.srsoftware.cal.api.Appointment; import de.srsoftware.cal.api.Appointment;
import de.srsoftware.tools.Error;
import de.srsoftware.tools.Result; import de.srsoftware.tools.Result;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
/** /**
@ -19,6 +22,8 @@ public interface Database {
*/ */
public Result<Appointment> add(Appointment appointment); public Result<Appointment> add(Appointment appointment);
Result<List<String>> findTags(String infix);
/** /**
* list appointments unfiltered * list appointments unfiltered
* @param count the maximum number of appointments to return * @param count the maximum number of appointments to return
@ -50,7 +55,7 @@ public interface Database {
Result<Appointment> loadEvent(String location, LocalDateTime start); Result<Appointment> loadEvent(String location, LocalDateTime start);
Result<List<String>> findTags(String infix); Result<Long> removeAppointment(long id);
Result<Appointment> update(Appointment baseAppointment); Result<Appointment> update(Appointment baseAppointment);
} }

32
de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java

@ -10,6 +10,7 @@ import static de.srsoftware.tools.jdbc.Query.*;
import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.*;
import de.srsoftware.cal.BaseAppointment; import de.srsoftware.cal.BaseAppointment;
import de.srsoftware.cal.Util;
import de.srsoftware.cal.api.Appointment; import de.srsoftware.cal.api.Appointment;
import de.srsoftware.cal.api.Attachment; import de.srsoftware.cal.api.Attachment;
import de.srsoftware.cal.api.Link; import de.srsoftware.cal.api.Link;
@ -251,17 +252,25 @@ public class MariaDB implements Database {
private Result<BaseAppointment> createAppointmentOf(ResultSet results) throws SQLException { private Result<BaseAppointment> createAppointmentOf(ResultSet results) throws SQLException {
var id = results.getInt(AID); var id = results.getInt(AID);
var title = results.getString("title"); var title = results.getString(TITLE);
var description = results.getString("description"); var description = results.getString(DESCRIPTION);
if (allEmpty(title, description)) return Error.format("Title and Description of appointment %s are empty", id); if (allEmpty(title, description)) return Error.format("Title and Description of appointment %s are empty", id);
var start = results.getTimestamp("start").toLocalDateTime(); var start = results.getTimestamp(START).toLocalDateTime();
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null); var end = nullable(results.getTimestamp(END)).map(Timestamp::toLocalDateTime).orElse(null);
var location = results.getString("location"); var location = results.getString(LOCATION);
var appointment = new BaseAppointment(id, title, description, start, end, 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 { try {
var tags = nullIfEmpty(results.getString("tags")); var tags = nullIfEmpty(results.getString("tags"));
if (tags != null) appointment.tags(tags.split(",")); if (tags != null) appointment.tags(tags.split(","));
} catch (SQLException e) { } catch (SQLException e) {
LOG.log(WARNING, "Failed to read tags from database!");
} }
return Payload.of(appointment); return Payload.of(appointment);
} }
@ -290,6 +299,19 @@ public class MariaDB implements Database {
return List.of(); 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 @Override
public Result<Appointment> update(Appointment event) { public Result<Appointment> update(Appointment event) {
var end = event.end().map(Timestamp::valueOf).orElse(null); var end = event.end().map(Timestamp::valueOf).orElse(null);

245
de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiHandler.java

@ -11,10 +11,8 @@ import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.cal.api.Appointment; import de.srsoftware.cal.api.Appointment;
import de.srsoftware.cal.api.Link; import de.srsoftware.cal.api.Link;
import de.srsoftware.cal.db.Database; import de.srsoftware.cal.db.Database;
import de.srsoftware.tools.*;
import de.srsoftware.tools.Error; 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.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
@ -22,6 +20,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.json.JSONObject; import org.json.JSONObject;
public class ApiHandler extends PathHandler { public class ApiHandler extends PathHandler {
@ -44,12 +43,42 @@ public class ApiHandler extends PathHandler {
case "/tags" -> listTags(ex,params); case "/tags" -> listTags(ex,params);
default -> PathHandler.notFound(ex); default -> PathHandler.notFound(ex);
}; };
} }
@Override @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) { 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); case "/event/edit" -> editEvent(ex);
default -> PathHandler.notFound(ex); default -> PathHandler.notFound(ex);
}; };
@ -58,129 +87,135 @@ public class ApiHandler extends PathHandler {
// spotless:off // spotless:off
private boolean editEvent(HttpExchange ex) throws IOException { private boolean editEvent(HttpExchange ex) throws IOException {
var json = json(ex); var json = json(ex);
var location = json.has(LOCATION) ? json.getString(LOCATION) : null; Optional<Appointment> existingAppointment = Optional.empty();
var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null; Long aid = json.has(AID) ? json.getLong(AID) : null;
if (allSet(location, start)) { if (aid != null) { // load appointment by aid
var existingAppointment = db.loadEvent(location, start).optional(); existingAppointment = db.loadEvent(aid).optional();
if (existingAppointment.isPresent()) { } else { // try to load appointment by location @ time
var event = existingAppointment.get(); var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
json.put(AID,event.id()); var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null;
return update(ex,toEvent(json)); 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); return createEvent(ex, json);
} }
// spotless:on // spotless:on
private Result<BaseAppointment> toEvent(JSONObject json) { private Result<BaseAppointment> toEvent(JSONObject json) {
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null; var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null; var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
if (title == null) return Error.of("title missing"); if (title == null) return Error.of("title missing");
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null; var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
if (start == null) return Error.of("start missing"); if (start == null) return Error.of("start missing");
var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); 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 end = json.has(END) ? nullIfEmpty(json.getString(END)) : null;
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
var location = json.has(LOCATION) ? json.getString(LOCATION) : null; var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
if (location == null) return Error.of("location missing"); if (location == null) return Error.of("location missing");
var aid = json.has(AID) ? json.getLong(AID) : 0; var aid = json.has(AID) ? json.getLong(AID) : 0;
var event = new BaseAppointment(aid, title, description, startDate, endDate, location); var event = new BaseAppointment(aid, title, description, startDate, endDate, location);
if (json.has(ATTACHMENTS)) { if (json.has(ATTACHMENTS)) {
json.getJSONArray(ATTACHMENTS).forEach(att -> { json.getJSONArray(ATTACHMENTS).forEach(att -> {
Payload // Payload //
.of(att.toString()) .of(att.toString())
.map(BaseImporter::url) .map(BaseImporter::url)
.map(BaseImporter::toAttachment) .map(BaseImporter::toAttachment)
.optional() .optional()
.ifPresent(event::add); .ifPresent(event::add);
}); });
} }
if (json.has(LINKS)) { if (json.has(LINKS)) {
json.getJSONArray(LINKS).forEach(o -> { json.getJSONArray(LINKS).forEach(o -> {
if (o instanceof JSONObject j) toLink(j).optional().ifPresent(event::addLinks); 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);
} }
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 { private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
var eventRes = toEvent(json); var eventRes = toEvent(json);
if (eventRes.optional().isPresent()) { if (eventRes.optional().isPresent()) {
return sendContent(ex, db.add(eventRes.optional().get()).map(ApiHandler::toJson)); return sendContent(ex, db.add(eventRes.optional().get()).map(ApiHandler::toJson));
}
return sendContent(ex, eventRes);
} }
return sendContent(ex, eventRes);
}
protected static Result<Link> toLink(JSONObject json) { protected static Result<Link> toLink(JSONObject json) {
try { try {
var description = json.getString(DESCRIPTION); var description = json.getString(DESCRIPTION);
return Payload.of(json.getString(URL)).map(BaseImporter::url).map(url -> BaseImporter.link(url, description)); return Payload.of(json.getString(URL)).map(BaseImporter::url).map(url -> BaseImporter.link(url, description));
} catch (Exception e) { } catch (Exception e) {
return Error.of("Failed to create link from %s".formatted(json), e); return Error.of("Failed to create link from %s".formatted(json), e);
}
} }
}
private boolean update(HttpExchange ex, Result<BaseAppointment> event) throws IOException { 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)); if (event.optional().isPresent()) return sendContent(ex, db.update(event.optional().get()).map(ApiHandler::toJson));
return sendContent(ex, event); 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) { private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException {
if (listResult.optional().isEmpty()) return listResult; var infix = params.get("infix");
List<String> list = listResult.optional().get(); if (infix == null) return sendContent(ex, Error.of("No infix set in method call parameters"));
while (list.size() > 15) { var res = db.findTags(infix).map(ApiHandler::sortTags);
int longest = list.stream().map(String::length).reduce(0, Integer::max); return sendContent(ex, res);
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 { private static Result<List<String>> sortTags(Result<List<String>> listResult) {
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null); if (listResult.optional().isEmpty()) return listResult;
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null); List<String> list = listResult.optional().get();
try { while (list.size() > 15) {
return PathHandler.sendContent(ex, db.list(start, end).stream().map(Appointment::json).toList()); int longest = list.stream().map(String::length).reduce(0, Integer::max);
} catch (SQLException e) { var subset = list.stream().filter(s -> s.length() < longest).toList();
LOG.log(WARNING, "Failed to fetch events (start = {0}, end = {1}!", start, end, e); if (subset.size() < 3) return Payload.of(list);
list = subset;
}
return Payload.of(list);
} }
return PathHandler.notFound(ex);
}
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException { private boolean listEvents(HttpExchange ex, Map<String, String> params) throws IOException {
var id = params.get("id"); var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
if (id != null) try { var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
return PathHandler.sendContent(ex, db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson)); try {
} catch (NumberFormatException | IOException nfe) { return PathHandler.sendContent(ex, db.list(start, end).stream().map(Appointment::json).toList());
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!", id)); } catch (SQLException e) {
LOG.log(WARNING, "Failed to fetch events (start = {0}, end = {1}!", start, end, e);
} }
return PathHandler.sendContent(ex, Error.of("ID missing")); return PathHandler.notFound(ex);
} }
private static Result<JSONObject> toJson(Result<Appointment> res) { private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
var opt = res.optional(); var id = params.get("id");
if (opt.isEmpty()) return Result.transform(res); if (id != null) try {
return Payload.of(opt.get().json()); 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) { private static LocalDateTime toLocalDateTime(String dateString) {
try { try {
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0); return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
} catch (Exception e) { } catch (Exception e) {
return null; return null;
}
} }
} }
}

9
de.srsoftware.cal.web/src/main/resources/edit.html

@ -9,9 +9,13 @@
<script src="/static/script/leaflet.js"></script> <script src="/static/script/leaflet.js"></script>
</head> </head>
<body> <body>
<h1>Create new event</h1> <h1>Create/Edit event</h1>
<button id="save" disabled>save</button> <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"> <div class="form">
<input type="hidden" id="aid" />
<fieldset> <fieldset>
<legend>basic data</legend> <legend>basic data</legend>
<table> <table>
@ -113,5 +117,6 @@
</div> </div>
<script>document.addEventListener("DOMContentLoaded", loadEvent);</script>
</body> </body>
</html> </html>

19
de.srsoftware.cal.web/src/main/resources/event.html

@ -5,20 +5,37 @@
<link rel="stylesheet" href="occ.css" /> <link rel="stylesheet" href="occ.css" />
<script src="/static/script/common.js"></script> <script src="/static/script/common.js"></script>
<script src="/static/script/event.js"></script> <script src="/static/script/event.js"></script>
<link rel="stylesheet" href="/static/leaflet.css" />
<script src="/static/script/leaflet.js"></script>
</head> </head>
<body class="event"> <body class="event">
<nav /> <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="time">Loading time…</div>
<div id="description">Loading description…</div> <div id="description">Loading description…</div>
<div id="links">Loading links…</div> <div id="links">Loading links…</div>
<div id="attachments">Loading attachments…</div> <div id="attachments">Loading attachments…</div>
<div id="tags">Loading tags…</div> <div id="tags">Loading tags…</div>
<div id="map" style="width: 100%; height: 500px;"></div>
<script> <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: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
document.addEventListener("DOMContentLoaded", function(event){ document.addEventListener("DOMContentLoaded", function(event){
console.log("page loaded…"); console.log("page loaded…");
loadEventData(); loadEventData();
}); });
</script> </script>
</body> </body>
</html> </html>

6
de.srsoftware.cal.web/src/main/resources/index.html

@ -27,16 +27,14 @@
</th> </th>
<th>End</th> <th>End</th>
<th>Location</th>
<th>Title</th> <th>Title</th>
<th>Location</th>
<th>Tags <span id="tag_selection"></span></th> <th>Tags <span id="tag_selection"></span></th>
</tr> </tr>
</table> </table>
</div> </div>
<script> <script>
document.addEventListener("DOMContentLoaded", function(event){ document.addEventListener("DOMContentLoaded", loadCurrentEvents);
loadCurrentEvents();
});
</script> </script>
<div id="overlay"> <div id="overlay">

24
de.srsoftware.cal.web/src/main/resources/occ.css

@ -48,14 +48,15 @@ body.event {
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
height: 20px; height: 25px;
width: 20px; width: 25px;
background: chocolate; background: chocolate;
z-index: 1000; z-index: 1000;
text-align: center; text-align: center;
border: 1px solid yellow; border: 1px solid yellow;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
font-size: 20px;
} }
#tag_selection { #tag_selection {
@ -127,15 +128,22 @@ table#eventlist{
display: none; display: none;
} }
#buttons {
position: fixed;
display: block;
text-align: center;
top: 10px;
width: 100%;
}
#cancel,
#save { #save {
min-width: 100px; min-width: 80px;
min-height: 50px; min-height: 30px;
font-size: 18px; font-size: 16px;
font-weight: bold; font-weight: bold;
position: fixed;
top: 15px;
left: 50%;
} }
.error { .error {
position: absolute; position: absolute;
left: 0; left: 0;

56
de.srsoftware.cal.web/src/main/resources/script/edit.js

@ -155,8 +155,10 @@ async function handleSave(response){
if (response.ok){ if (response.ok){
var json = await response.json(); var json = await response.json();
var url = window.location.href.replace('/static/edit','') var url = window.location.href.replace('/static/edit','')
if (json.id) url+='?highlight='+json.id; let pos = url.indexOf('?');
window.location.href = url; // TODO: add highlight on new event if (pos>0) url = url.substring(0,pos);
if (json.id) url+='?id='+json.id;
window.location.href = url;
} else { } else {
var json = await response.json(); var json = await response.json();
if (json.error) showError(json.error); if (json.error) showError(json.error);
@ -169,6 +171,7 @@ async function saveEvent(){
var location = element('location').value; var location = element('location').value;
var start = element('start').value; var start = element('start').value;
var event = { var event = {
aid : element('aid').value,
title : element('title').value, title : element('title').value,
description : element('description').value, description : element('description').value,
location : element('location').value, location : element('location').value,
@ -187,3 +190,52 @@ async function saveEvent(){
} }
}).then(handleSave); }).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);
}

51
de.srsoftware.cal.web/src/main/resources/script/event.js

@ -17,17 +17,21 @@ function attachmentList(json){
} }
async function handleEventData(response){ async function handleEventData(response){
if (response.ok){ if (response.ok){
var json = await response.json(); var event = await response.json();
document.getElementsByTagName('h1')[0].innerHTML = json.title ? json.title : ''; document.getElementsByTagName('h1')[0].innerHTML = event.title ? event.title : '';
document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : ''); document.getElementById('time').innerHTML = event.start + (event.end ? '…'+event.end : '');
addDescription(json); addDescription(event);
document.getElementById('tags').innerHTML = "Tags: "+json.tags.join(" "); document.getElementById('tags').innerHTML = "Tags: "+event.tags.join(" ");
var links = document.getElementById('links'); var links = document.getElementById('links');
links.innerHTML = ""; links.innerHTML = "";
links.appendChild(linkList(json.links)); links.appendChild(linkList(event.links));
attachmentList(json.attachments); 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){ function linkList(json){
@ -50,3 +54,28 @@ function loadEventData(){
if (id) fetch('/api/event?id='+id).then(handleEventData); 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);
}
}

19
de.srsoftware.cal.web/src/main/resources/script/index.js

@ -23,8 +23,8 @@ function addRow(json){
addCell(row,json.id,json.id); addCell(row,json.id,json.id);
addCell(row,json.start,json.id); addCell(row,json.start,json.id);
addCell(row,json.end,json.id); addCell(row,json.end,json.id);
addCell(row,json.location,json.id);
addCell(row,json.title,json.id); addCell(row,json.title,json.id);
addCell(row,json.location,json.id);
row.appendChild(createTags(json.tags)); row.appendChild(createTags(json.tags));
} }
@ -80,15 +80,17 @@ async function handleEvents(response){
updateTagVisibility(); updateTagVisibility();
if (highlight){ if (highlight){
var row = element(highlight); var row = element(highlight);
row.classList.add('highlight'); if (row) {
row.scrollIntoView({behavior: 'smooth',block: 'center'}); row.classList.add('highlight');
row.scrollIntoView({behavior: 'smooth',block: 'center'});
}
} }
} }
} }
function loadCurrentEvents(){ function loadCurrentEvents(){
let params = new URLSearchParams(location.search); let params = new URLSearchParams(location.search);
highlight = params.get('highlight'); highlight = params.get('id');
if (start == null){ if (start == null){
var now = new Date(); var now = new Date();
var year = now.getFullYear(); var year = now.getFullYear();
@ -106,10 +108,11 @@ function showOverlay(url){
div.appendChild(iframe); div.appendChild(iframe);
div.style.display = 'block'; div.style.display = 'block';
div.onclick = e => div.style.display = 'none'; div.onclick = e => div.style.display = 'none';
var btn = document.createElement('div'); var closeBtn = document.createElement('div');
btn.id = 'close_overlay'; closeBtn.id = 'close_overlay';
btn.innerHTML = '✖'; closeBtn.innerHTML = '✖';
div.appendChild(btn); closeBtn.title = 'close overlay';
div.appendChild(closeBtn);
} }
function toggleTag(tag){ function toggleTag(tag){

Loading…
Cancel
Save