Add path prefix

This commit is contained in:
Lars Westermann 2019-06-10 10:09:36 +02:00
parent 19956ebafb
commit afbced61e3
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
39 changed files with 656 additions and 305 deletions

View file

@ -1,6 +1,8 @@
[server] [server]
host = "localhost" host = "localhost"
port = 8080 port = 8080
prefix = ""
debug = false
[schedule] [schedule]
reference = "2019-06-12" reference = "2019-06-12"

View file

@ -11,7 +11,8 @@ import org.w3c.dom.events.Event
import kotlin.browser.window import kotlin.browser.window
class WebSocketClient() { class WebSocketClient() {
private val url = "ws://${window.location.host}/" val prefix = js("prefix")
private val url = "ws://${window.location.host}$prefix/"
private lateinit var ws: WebSocket private lateinit var ws: WebSocket
private var reconnect = false private var reconnect = false

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object PostRepository : Repository<Post> { object PostRepository : Repository<Post> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>() override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>() override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>() override val onDelete = EventHandler<Long>()
@ -18,35 +20,35 @@ object PostRepository : Repository<Post> {
private val parser = DynamicObjectParser() private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Post? { override suspend fun get(id: Long): Post? {
val json = repositoryGet("/api/post/$id") ?: return null val json = repositoryGet("$prefix/api/post/$id") ?: return null
return parser.parse(json, Post.serializer()) return parser.parse(json, Post.serializer())
} }
override suspend fun create(model: Post): Long { override suspend fun create(model: Post): Long {
return repositoryPost("/api/posts", Message.json.stringify(Post.serializer(), model)) return repositoryPost("$prefix/api/posts", Message.json.stringify(Post.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Post) { override suspend fun update(model: Post) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/post/${model.id}", Message.json.stringify(Post.serializer(), model)) repositoryPost("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
repositoryPost("/api/post/$id/delete") repositoryPost("$prefix/api/post/$id/delete")
} }
override suspend fun all(): List<Post> { override suspend fun all(): List<Post> {
val json = repositoryGet("/api/posts") ?: return emptyList() val json = repositoryGet("$prefix/api/posts") ?: return emptyList()
return parser.parse(json, Post.serializer().list) return parser.parse(json, Post.serializer().list)
} }
suspend fun htmlByUrl(url: String): String { suspend fun htmlByUrl(url: String): String {
return repositoryRawGet("/api/p/$url") return repositoryRawGet("$prefix/api/p/$url")
} }
suspend fun render(data: String): String { suspend fun render(data: String): String {
return repositoryPost("/api/render", data) return repositoryPost("$prefix/api/render", data)
} }
val handler = object : MessageHandler(RepositoryType.POST) { val handler = object : MessageHandler(RepositoryType.POST) {

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object RoomRepository : Repository<Room> { object RoomRepository : Repository<Room> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>() override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>() override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>() override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object RoomRepository : Repository<Room> {
private val parser = DynamicObjectParser() private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Room? { override suspend fun get(id: Long): Room? {
val json = repositoryGet("/api/room/$id") ?: return null val json = repositoryGet("$prefix/api/room/$id") ?: return null
return parser.parse(json, Room.serializer()) return parser.parse(json, Room.serializer())
} }
override suspend fun create(model: Room): Long { override suspend fun create(model: Room): Long {
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model)) return repositoryPost("$prefix/api/rooms", Message.json.stringify(Room.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Room) { override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/room/${model.id}", Message.json.stringify(Room.serializer(), model)) repositoryPost("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
repositoryPost("/api/room/$id/delete") repositoryPost("$prefix/api/room/$id/delete")
} }
override suspend fun all(): List<Room> { override suspend fun all(): List<Room> {
val json = repositoryGet("/api/rooms") ?: return emptyList() val json = repositoryGet("$prefix/api/rooms") ?: return emptyList()
return parser.parse(json, Room.serializer().list) return parser.parse(json, Room.serializer().list)
} }

View file

@ -12,6 +12,8 @@ import kotlinx.serialization.list
object ScheduleRepository : Repository<Schedule> { object ScheduleRepository : Repository<Schedule> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>() override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>() override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>() override val onDelete = EventHandler<Long>()
@ -19,26 +21,26 @@ object ScheduleRepository : Repository<Schedule> {
private val parser = DynamicObjectParser() private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Schedule? { override suspend fun get(id: Long): Schedule? {
val json = repositoryGet("/api/schedule/$id") ?: return null val json = repositoryGet("$prefix/api/schedule/$id") ?: return null
return parser.parse(json, Schedule.serializer()) return parser.parse(json, Schedule.serializer())
} }
override suspend fun create(model: Schedule): Long { override suspend fun create(model: Schedule): Long {
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model)) return repositoryPost("$prefix/api/schedules", Message.json.stringify(Schedule.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Schedule) { override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model)) repositoryPost("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
repositoryPost("/api/schedule/$id/delete") repositoryPost("$prefix/api/schedule/$id/delete")
} }
override suspend fun all(): List<Schedule> { override suspend fun all(): List<Schedule> {
val json = repositoryGet("/api/schedules") ?: return emptyList() val json = repositoryGet("$prefix/api/schedules") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list) return parser.parse(json, Schedule.serializer().list)
} }
@ -52,12 +54,12 @@ object ScheduleRepository : Repository<Schedule> {
} }
suspend fun checkConstraints(): ConstraintMap { suspend fun checkConstraints(): ConstraintMap {
val json = repositoryGet("/api/constraints") val json = repositoryGet("$prefix/api/constraints")
return parser.parse(json, ConstraintMap.serializer()) return parser.parse(json, ConstraintMap.serializer())
} }
suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap { suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap {
val json = repositoryGet("/api/constraint/${schedule.id}") val json = repositoryGet("$prefix/api/constraint/${schedule.id}")
return parser.parse(json, ConstraintMap.serializer()) return parser.parse(json, ConstraintMap.serializer())
} }
} }

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object TrackRepository : Repository<Track> { object TrackRepository : Repository<Track> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>() override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>() override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>() override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object TrackRepository : Repository<Track> {
private val parser = DynamicObjectParser() private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Track? { override suspend fun get(id: Long): Track? {
val json = repositoryGet("/api/track/$id") ?: return null val json = repositoryGet("$prefix/api/track/$id") ?: return null
return parser.parse(json, Track.serializer()) return parser.parse(json, Track.serializer())
} }
override suspend fun create(model: Track): Long { override suspend fun create(model: Track): Long {
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model)) return repositoryPost("$prefix/api/tracks", Message.json.stringify(Track.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Track) { override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/track/${model.id}", Message.json.stringify(Track.serializer(), model)) repositoryPost("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
repositoryPost("/api/track/$id/delete") repositoryPost("$prefix/api/track/$id/delete")
} }
override suspend fun all(): List<Track> { override suspend fun all(): List<Track> {
val json = repositoryGet("/api/tracks") ?: return emptyList() val json = repositoryGet("$prefix/api/tracks") ?: return emptyList()
return parser.parse(json, Track.serializer().list) return parser.parse(json, Track.serializer().list)
} }

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object UserRepository : Repository<User> { object UserRepository : Repository<User> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>() override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>() override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>() override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object UserRepository : Repository<User> {
private val parser = DynamicObjectParser() private val parser = DynamicObjectParser()
override suspend fun get(id: Long): User? { override suspend fun get(id: Long): User? {
val json = repositoryGet("/api/user/$id") ?: return null val json = repositoryGet("$prefix/api/user/$id") ?: return null
return parser.parse(json, User.serializer()) return parser.parse(json, User.serializer())
} }
override suspend fun create(model: User): Long { override suspend fun create(model: User): Long {
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model)) return repositoryPost("$prefix/api/users", Message.json.stringify(User.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: User) { override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/user/${model.id}", Message.json.stringify(User.serializer(), model)) repositoryPost("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
repositoryPost("/api/user/$id/delete") repositoryPost("$prefix/api/user/$id/delete")
} }
override suspend fun all(): List<User> { override suspend fun all(): List<User> {
val json = repositoryGet("/api/users") ?: return emptyList() val json = repositoryGet("$prefix/api/users") ?: return emptyList()
return parser.parse(json, User.serializer().list) return parser.parse(json, User.serializer().list)
} }

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object WorkGroupRepository : Repository<WorkGroup> { object WorkGroupRepository : Repository<WorkGroup> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>() override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>() override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>() override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object WorkGroupRepository : Repository<WorkGroup> {
private val parser = DynamicObjectParser() private val parser = DynamicObjectParser()
override suspend fun get(id: Long): WorkGroup? { override suspend fun get(id: Long): WorkGroup? {
val json = repositoryGet("/api/workgroup/$id") ?: return null val json = repositoryGet("$prefix/api/workgroup/$id") ?: return null
return parser.parse(json, WorkGroup.serializer()) return parser.parse(json, WorkGroup.serializer())
} }
override suspend fun create(model: WorkGroup): Long { override suspend fun create(model: WorkGroup): Long {
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model)) return repositoryPost("$prefix/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: WorkGroup) { override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model)) repositoryPost("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
repositoryPost("/api/workgroup/$id/delete") repositoryPost("$prefix/api/workgroup/$id/delete")
} }
override suspend fun all(): List<WorkGroup> { override suspend fun all(): List<WorkGroup> {
val json = repositoryGet("/api/workgroups") ?: return emptyList() val json = repositoryGet("$prefix/api/workgroups") ?: return emptyList()
return parser.parse(json, WorkGroup.serializer().list) return parser.parse(json, WorkGroup.serializer().list)
} }

View file

@ -47,6 +47,7 @@ fun initWorkGroupConstraints() {
html.name = "constraint-only-on-day-${index++}" html.name = "constraint-only-on-day-${index++}"
min = -1337.0 min = -1337.0
max = 1337.0 max = 1337.0
placeholder = "Tag"
}.html) }.html)
}.html) }.html)
} }
@ -64,6 +65,7 @@ fun initWorkGroupConstraints() {
html.name = "constraint-not-on-day-${index++}" html.name = "constraint-not-on-day-${index++}"
min = -1337.0 min = -1337.0
max = 1337.0 max = 1337.0
placeholder = "Tag"
}.html) }.html)
}.html) }.html)
} }
@ -78,13 +80,15 @@ fun initWorkGroupConstraints() {
}.html) }.html)
html.appendChild(InputView(InputType.TEXT).apply { html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control" classList += "form-control"
html.name = "constraint-only-before-time-day-${index++}" html.name = "constraint-only-before-time-day-${index}"
placeholder = "Tag (optional)"
}.html) }.html)
html.appendChild(InputView(InputType.NUMBER).apply { html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control" classList += "form-control"
html.name = "constraint-only-before-time-${index++}" html.name = "constraint-only-before-time-${index++}"
min = -1337.0 min = -1337.0
max = 133700.0 max = 133700.0
placeholder = "Minuten"
}.html) }.html)
}.html) }.html)
} }
@ -99,13 +103,15 @@ fun initWorkGroupConstraints() {
}.html) }.html)
html.appendChild(InputView(InputType.TEXT).apply { html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control" classList += "form-control"
html.name = "constraint-only-after-time-day-${index++}" html.name = "constraint-only-after-time-day-${index}"
placeholder = "Tag (optional)"
}.html) }.html)
html.appendChild(InputView(InputType.NUMBER).apply { html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control" classList += "form-control"
html.name = "constraint-only-after-time-${index++}" html.name = "constraint-only-after-time-${index++}"
min = -1337.0 min = -1337.0
max = 133700.0 max = 133700.0
placeholder = "Minuten"
}.html) }.html)
}.html) }.html)
} }

View file

@ -9,17 +9,20 @@ import de.westermann.kwebview.iterator
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.browser.document import kotlin.browser.document
import kotlin.browser.window import kotlin.browser.window
import kotlin.js.Date
class Calendar(calendar: HTMLElement) : View(calendar) { class Calendar(calendar: HTMLElement) : View(calendar) {
var autoScroll = true var autoScroll = true
val day: Int = calendar.dataset["day"]?.toIntOrNull() ?: -1
val calendarTable = calendar.getElementsByClassName("calendar-table")[0] as HTMLElement val calendarTable = calendar.getElementsByClassName("calendar-table")[0] as HTMLElement
private val calendarTableHeader = calendar.getElementsByClassName("calendar-header")[0] as HTMLElement private val calendarTableHeader = calendar.getElementsByClassName("calendar-header")[0] as HTMLElement
val day = calendarTable.dataset["day"]?.toIntOrNull() ?: -1
val referenceDate = calendarTable.dataset["reference"]?.toLongOrNull() ?: -1L
val nowDate = calendarTable.dataset["now"]?.toLongOrNull() ?: -1L
val timeDifference = Date.now().toLong() - nowDate
fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) { fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
scrollAllVerticalBy(pixel, scrollBehavior) scrollAllVerticalBy(pixel, scrollBehavior)
} }
@ -46,7 +49,6 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
Orientation.ROOM_TO_TIME Orientation.ROOM_TO_TIME
} }
init { init {
scroll += calendarTable scroll += calendarTable
@ -131,7 +133,6 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
private var scroll = listOf<HTMLElement>() private var scroll = listOf<HTMLElement>()
private fun scrollAllVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) { private fun scrollAllVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
println("scroll ${scroll.size} elemenets")
for (calendarTable in scroll) { for (calendarTable in scroll) {
calendarTable.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior)) calendarTable.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior))
} }

View file

@ -86,9 +86,14 @@ class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection<C
} }
fun update(scroll: ScrollBehavior) { fun update(scroll: ScrollBehavior) {
val currentTime = Date().let { val now = Date.now().toLong() - calendar.timeDifference
it.getHours() * 60 + it.getMinutes() val refDay = calendar.referenceDate / (1000 * 60 * 60 * 24)
} val nowDay = now / (1000 * 60 * 60 * 24)
val d = nowDay - refDay
val diff = (day - d).toInt()
val date = Date(now)
val currentTime = date.getHours() * 60 + date.getMinutes() + (diff * 60 * 24)
val rowTime = (currentTime / 15) * 15 val rowTime = (currentTime / 15) * 15
var activeRow: CalendarRow? = null var activeRow: CalendarRow? = null
@ -116,7 +121,7 @@ class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection<C
if (calendar.autoScroll && activeRow != null) { if (calendar.autoScroll && activeRow != null) {
if (calendar.orientation == Calendar.Orientation.ROOM_TO_TIME) { if (calendar.orientation == Calendar.Orientation.ROOM_TO_TIME) {
calendar.scrollVerticalTo((activeRow.offsetTop).toDouble(), scroll) calendar.scrollVerticalTo((activeRow.offsetTop - 150).toDouble(), scroll)
} else { } else {
calendar.scrollHorizontalTo((activeRow.offsetLeft - 100).toDouble(), scroll) calendar.scrollHorizontalTo((activeRow.offsetLeft - 100).toDouble(), scroll)
} }

View file

@ -7,10 +7,13 @@ import de.westermann.kwebview.iterator
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.ScheduleRepository import de.kif.frontend.repository.ScheduleRepository
import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kwebview.* import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body import de.westermann.kwebview.components.Body
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.events.MouseEvent import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.browser.window import kotlin.browser.window
import kotlin.dom.appendText import kotlin.dom.appendText
import kotlin.dom.isText import kotlin.dom.isText
@ -19,6 +22,7 @@ import kotlin.js.Date
class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : View(view) { class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point private lateinit var mouseDelta: Point
private var ignoreEditHover = false
private var newCell: CalendarCell? = null private var newCell: CalendarCell? = null
private var language by dataset.property("language") private var language by dataset.property("language")
@ -43,9 +47,20 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie
private var moveLockRoom: Long? = null private var moveLockRoom: Long? = null
private var moveLockTime: Int? = null private var moveLockTime: Int? = null
private val nameView = view.getByClassOrCreate<HTMLSpanElement>("calendar-entry-name")
private fun onMove(event: MouseEvent) { private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta val position = event.toPoint() - mouseDelta
if (ignoreEditHover) {
for (element in document.elementsFromPoint(position.x, position.y)) {
if (element.classList.contains("calendar-edit")) {
return
}
}
ignoreEditHover = false
}
val cell = calendar.calendarCells.find { val cell = calendar.calendarCells.find {
position in it.dimension position in it.dimension
} }
@ -245,13 +260,7 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie
} }
} }
for (element in html.childNodes) { nameView.textContent = workGroup.name
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(workGroup.name)
} }
companion object { companion object {
@ -268,6 +277,7 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie
entry.load(workGroup) entry.load(workGroup)
entry.mouseDelta = Point.ZERO entry.mouseDelta = Point.ZERO
entry.ignoreEditHover = true
entry.startDrag() entry.startDrag()
return entry return entry

View file

@ -1,6 +1,10 @@
$border-radius: 0.2rem; $border-radius: 0.2rem;
$transitionTime: 150ms; $transitionTime: 150ms;
$mainFont: 'Raleway', Roboto, Arial, sans-serif;
$menuFont: 'Bungee', 'Raleway', Roboto, Arial, sans-serif;
$headFont: 'Montserrat', 'Raleway', Roboto, Arial, sans-serif;
@mixin no-select() { @mixin no-select() {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;

View file

@ -213,6 +213,8 @@
span:first-child { span:first-child {
color: var(--text-primary-color); color: var(--text-primary-color);
width: 6rem; width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
&:hover { &:hover {
background-color: transparent; background-color: transparent;
@ -297,6 +299,13 @@
@include no-select() @include no-select()
} }
.calendar-entry-name {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.calendar-table-box { .calendar-table-box {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -404,7 +413,7 @@
left: 6rem; left: 6rem;
right: 0; right: 0;
height: 1px; height: 1px;
border-bottom: solid 1px $primary-color; border-bottom: solid 1px var(--primary-color);
} }
&.calendar-now::after { &.calendar-now::after {
@ -414,8 +423,8 @@
left: 6rem; left: 6rem;
transform: scale(1, 0.5) rotate(-45deg); transform: scale(1, 0.5) rotate(-45deg);
transform-origin: bottom; transform-origin: bottom;
border-bottom: solid 0.4rem $primary-color; border-bottom: solid 0.4rem var(--primary-color);
border-right: solid 0.4rem $primary-color; border-right: solid 0.4rem var(--primary-color);
border-top: solid 0.4rem transparent; border-top: solid 0.4rem transparent;
border-left: solid 0.4rem transparent; border-left: solid 0.4rem transparent;
margin-top: -0.55rem; margin-top: -0.55rem;
@ -498,7 +507,7 @@
top: 3rem; top: 3rem;
bottom: 0; bottom: 0;
width: 1px; width: 1px;
border-right: solid 1px $primary-color; border-right: solid 1px var(--primary-color);
} }
&.calendar-now::after { &.calendar-now::after {
@ -508,8 +517,8 @@
top: 3rem; top: 3rem;
transform: scale(0.5, 1) rotate(45deg); transform: scale(0.5, 1) rotate(45deg);
transform-origin: right; transform-origin: right;
border-bottom: solid 0.4rem $primary-color; border-bottom: solid 0.4rem var(--primary-color);
border-right: solid 0.4rem $primary-color; border-right: solid 0.4rem var(--primary-color);
border-top: solid 0.4rem transparent; border-top: solid 0.4rem transparent;
border-left: solid 0.4rem transparent; border-left: solid 0.4rem transparent;
margin-top: -0.3rem; margin-top: -0.3rem;

View file

@ -13,7 +13,7 @@
color: var(--text-primary-color); color: var(--text-primary-color);
height: 100%; height: 100%;
display: inline-block; display: inline-block;
font-family: "Bungee", sans-serif; font-family: $menuFont;
font-weight: normal; font-weight: normal;
font-size: 1.1rem; font-size: 1.1rem;
position: relative; position: relative;

View file

@ -33,7 +33,7 @@
line-height: 2rem; line-height: 2rem;
padding: 0 1rem; padding: 0 1rem;
font-weight: bold; font-weight: bold;
font-family: 'Montserrat', sans-serif; font-family: $headFont;
&:empty::before { &:empty::before {
display: block; display: block;

View file

@ -39,6 +39,7 @@
margin-top: -0.5rem !important; margin-top: -0.5rem !important;
bottom: 0 !important; bottom: 0 !important;
} }
.calendar-entry::after { .calendar-entry::after {
content: none; content: none;
} }
@ -65,6 +66,12 @@
line-height: 2rem; line-height: 2rem;
width: 100%; width: 100%;
} }
.calendar-header {
.calendar-cell {
overflow: hidden;
}
}
} }
.wall-calendar { .wall-calendar {

View file

@ -15,7 +15,7 @@ body, html {
color: var(--text-primary-color); color: var(--text-primary-color);
background: var(--background-secondary-color); background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif; font-family: $mainFont;
font-weight: 500; font-weight: 500;
width: 100%; width: 100%;
@ -30,6 +30,10 @@ body, html {
} }
} }
h1, h2, h3, h4, h5, h6 {
font-family: $headFont;
}
.no-select { .no-select {
@include no-select() @include no-select()
} }

View file

@ -13,13 +13,21 @@ import io.ktor.http.content.files
import io.ktor.http.content.static import io.ktor.http.content.static
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import org.slf4j.event.Level
import java.nio.file.Paths import java.nio.file.Paths
val prefix = Configuration.Server.prefix
fun Application.main() { fun Application.main() {
install(DefaultHeaders) install(DefaultHeaders)
install(CallLogging) install(CallLogging) {
if (Configuration.Server.debug) {
level = Level.INFO
}
}
install(ConditionalHeaders) install(ConditionalHeaders)
install(Compression) install(Compression)
install(DataConversion) install(DataConversion)
@ -42,47 +50,50 @@ fun Application.main() {
security() security()
routing { routing {
static("/static") {
files(Configuration.Path.webPath.toFile())
}
static("/images") { route(prefix) {
files(Configuration.Path.uploadsPath.toFile()) static("/static") {
} files(Configuration.Path.webPath.toFile())
val srcFile = Paths.get("src")?.toFile()
if (srcFile != null && srcFile.exists() && srcFile.isDirectory) {
static("/src") {
files("src")
} }
static("/images") {
files(Configuration.Path.uploadsPath.toFile())
}
val srcFile = Paths.get("src")?.toFile()
if (srcFile != null && srcFile.exists() && srcFile.isDirectory) {
static("/src") {
files("src")
}
}
// UI routes
overview()
calendar()
login()
account()
board()
wall()
workGroup()
track()
room()
user()
// RESTful routes
authenticateApi()
roomApi()
scheduleApi()
trackApi()
userApi()
workGroupApi()
constraintsApi()
postApi()
// Web socket push notifications
pushService()
} }
// UI routes
overview()
calendar()
login()
account()
board()
wall()
workGroup()
track()
room()
user()
// RESTful routes
authenticateApi()
roomApi()
scheduleApi()
trackApi()
userApi()
workGroupApi()
constraintsApi()
postApi()
// Web socket push notifications
pushService()
} }
} }

View file

@ -26,11 +26,15 @@ object Configuration {
private object ServerSpec : ConfigSpec("server") { private object ServerSpec : ConfigSpec("server") {
val host by required<String>() val host by required<String>()
val port by required<Int>() val port by required<Int>()
val debug by required<Boolean>()
val cPrefix by required<String>("prefix")
} }
object Server { object Server {
val host by c(ServerSpec.host) val host by c(ServerSpec.host)
val port by c(ServerSpec.port) val port by c(ServerSpec.port)
val debug by c(ServerSpec.debug)
val prefix by c(ServerSpec.cPrefix)
} }
private object PathSpec : ConfigSpec("path") { private object PathSpec : ConfigSpec("path") {

View file

@ -31,7 +31,7 @@ fun main(args: Array<String>) {
var password: String? = null var password: String? = null
while (password == null) { while (password == null) {
print("Password: ") print("Password: ")
password = System.console()?.readPassword()?.toString() ?: readLine() password = System.console()?.readPassword()?.joinToString("") ?: readLine()
} }
println("Create root user '$username' with pw '${"*".repeat(password.length)}'") println("Create root user '$username' with pw '${"*".repeat(password.length)}'")

View file

@ -14,6 +14,7 @@ import io.ktor.sessions.*
import io.ktor.util.hex import io.ktor.util.hex
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import de.kif.backend.prefix
interface ErrorContext { interface ErrorContext {
suspend infix fun onFailure(block: suspend () -> Unit) suspend infix fun onFailure(block: suspend () -> Unit)
@ -49,7 +50,7 @@ suspend inline fun PipelineContext<Unit, ApplicationCall>.authenticateOrRedirect
block: (user: User) -> Unit block: (user: User) -> Unit
) { ) {
authenticate(*permissions, block = block) onFailure { authenticate(*permissions, block = block) onFailure {
call.respondRedirect("/login?redirect=${call.request.path()}}") call.respondRedirect("$prefix/login?redirect=${call.request.path()}")
} }
} }
@ -68,7 +69,7 @@ data class PortalSession(val userId: Long) {
if (user == null) { if (user == null) {
call.sessions.clear<PortalSession>() call.sessions.clear<PortalSession>()
call.respondRedirect("/login?onFailure") call.respondRedirect("$prefix/login?onFailure")
throw IllegalAccessException() throw IllegalAccessException()
} }
@ -93,7 +94,7 @@ fun Application.security() {
userParamName = "username" userParamName = "username"
passwordParamName = "password" passwordParamName = "password"
challenge = FormAuthChallenge.Redirect { _ -> challenge = FormAuthChallenge.Redirect { _ ->
"/login?onFailure" "$prefix/login?onFailure"
} }
validate { credential: UserPasswordCredential -> validate { credential: UserPasswordCredential ->
val user = UserRepository.find(credential.name) ?: return@validate null val user = UserRepository.find(credential.name) ?: return@validate null

View file

@ -26,6 +26,7 @@ import io.ktor.routing.post
import io.ktor.util.toMap import io.ktor.util.toMap
import kotlinx.html.* import kotlinx.html.*
import mu.KotlinLogging import mu.KotlinLogging
import de.kif.backend.prefix
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@ -40,7 +41,7 @@ fun Route.account() {
br {} br {}
+"Du hast folgende Rechte: ${user.permissions.joinToString(", ") { it.germanInfo }}" +"Du hast folgende Rechte: ${user.permissions.joinToString(", ") { it.germanInfo }}"
br {} br {}
a("/logout") { a("$prefix/logout") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Ausloggen" +"Ausloggen"
} }
@ -69,25 +70,25 @@ fun Route.account() {
div("account-backup") { div("account-backup") {
if (user.checkPermission(Permission.ROOM)) { if (user.checkPermission(Permission.ROOM)) {
a("/account/backup/rooms.json", classes = "form-btn") { a("$prefix/account/backup/rooms.json", classes = "form-btn") {
attributes["download"] = "rooms-backup.json" attributes["download"] = "rooms-backup.json"
+"Räume sichern" +"Räume sichern"
} }
} }
if (user.checkPermission(Permission.USER)) { if (user.checkPermission(Permission.USER)) {
a("/account/backup/users.json", classes = "form-btn") { a("$prefix/account/backup/users.json", classes = "form-btn") {
attributes["download"] = "users-backup.json" attributes["download"] = "users-backup.json"
+"Nutzer sichern" +"Nutzer sichern"
} }
} }
if (user.checkPermission(Permission.POST)) { if (user.checkPermission(Permission.POST)) {
a("/account/backup/posts.json", classes = "form-btn") { a("$prefix/account/backup/posts.json", classes = "form-btn") {
attributes["download"] = "posts-backup.json" attributes["download"] = "posts-backup.json"
+"Beiträge sichern" +"Beiträge sichern"
} }
} }
if (user.checkPermission(Permission.WORK_GROUP)) { if (user.checkPermission(Permission.WORK_GROUP)) {
a("/account/backup/work-groups.json", classes = "form-btn") { a("$prefix/account/backup/work-groups.json", classes = "form-btn") {
attributes["download"] = "work-groups-backup.json" attributes["download"] = "work-groups-backup.json"
+"Arbeitskreise sichern" +"Arbeitskreise sichern"
} }
@ -97,14 +98,14 @@ fun Route.account() {
user.checkPermission(Permission.ROOM) && user.checkPermission(Permission.ROOM) &&
user.checkPermission(Permission.SCHEDULE) user.checkPermission(Permission.SCHEDULE)
) { ) {
a("/account/backup/schedules.json", classes = "form-btn") { a("$prefix/account/backup/schedules.json", classes = "form-btn") {
attributes["download"] = "schedules-backup.json" attributes["download"] = "schedules-backup.json"
+"Zeitplan sichern" +"Zeitplan sichern"
} }
} }
if (user.checkPermission(Permission.ADMIN)) { if (user.checkPermission(Permission.ADMIN)) {
a("/account/backup.json", classes = "form-btn") { a("$prefix/account/backup.json", classes = "form-btn") {
attributes["download"] = "backup.json" attributes["download"] = "backup.json"
+"Alles sichern" // TODO: richtiger Text? +"Alles sichern" // TODO: richtiger Text?
} }
@ -179,7 +180,7 @@ fun Route.account() {
div("account-import-wiki") { div("account-import-wiki") {
span { +"Arbeitskreise aus dem KIF-Wiki importieren" } span { +"Arbeitskreise aus dem KIF-Wiki importieren" }
form(action = "/account/import", method = FormMethod.post) { form(action = "$prefix/account/import", method = FormMethod.post) {
for ((index, section) in wikiSections.withIndex()) { for ((index, section) in wikiSections.withIndex()) {
div("form-group") { div("form-group") {
label { label {
@ -330,7 +331,7 @@ fun Route.account() {
Backup.import(import) Backup.import(import)
} }
call.respondRedirect("/account") call.respondRedirect("$prefix/account")
} onFailure { } onFailure {
call.error(HttpStatusCode.Unauthorized) call.error(HttpStatusCode.Unauthorized)
} }
@ -387,7 +388,7 @@ fun Route.account() {
logger.info { "Import $counter from ${importedWorkGroups.size} work groups!" } logger.info { "Import $counter from ${importedWorkGroups.size} work groups!" }
call.respondRedirect("/account") call.respondRedirect("$prefix/account")
} onFailure { } onFailure {
call.error(HttpStatusCode.Unauthorized) call.error(HttpStatusCode.Unauthorized)
} }

View file

@ -1,16 +1,17 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.Configuration import de.kif.backend.Configuration
import de.kif.backend.repository.PostRepository
import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.respondMain import de.kif.backend.view.respondMain
import de.kif.common.model.Schedule
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import kotlinx.css.CSSBuilder import kotlinx.css.CSSBuilder
import kotlinx.css.Color import kotlinx.css.Color
import kotlinx.html.* import kotlinx.html.div
import kotlinx.html.img
import kotlinx.html.span
import kotlinx.html.unsafe
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -120,20 +121,19 @@ fun Route.board() {
} }
div("board-content") { div("board-content") {
div("board-calendar calendar") { div("board-calendar calendar") {
div("calendar-table") { renderCalendar(
renderCalendar( CalendarOrientation.ROOM_TO_TIME,
CalendarOrientation.ROOM_TO_TIME, day,
day, min,
min, max,
max, rooms,
rooms, schedules
schedules )
)
}
} }
div("board-twitter") { div("board-twitter") {
unsafe { unsafe {
raw(""" raw(
"""
<a <a
class="twitter-timeline" class="twitter-timeline"
href="${Configuration.Twitter.timeline}" href="${Configuration.Twitter.timeline}"
@ -145,7 +145,8 @@ fun Route.board() {
data-dnt="true" data-dnt="true"
>Twitter wall</a> >Twitter wall</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
""".trimIndent()) """.trimIndent()
)
} }
} }
} }

View file

@ -26,6 +26,7 @@ import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import de.kif.backend.prefix
const val MINUTES_OF_DAY = 24 * 60 const val MINUTES_OF_DAY = 24 * 60
@ -53,7 +54,9 @@ private fun DIV.calendarCell(schedule: Schedule?) {
attributes["data-time"] = schedule.time.toString() attributes["data-time"] = schedule.time.toString()
attributes["data-length"] = schedule.workGroup.length.toString() attributes["data-length"] = schedule.workGroup.length.toString()
+schedule.workGroup.name span("calendar-entry-name") {
+schedule.workGroup.name
}
} }
} }
} }
@ -70,66 +73,75 @@ fun DIV.renderCalendar(
val minutesOfDay = to - from val minutesOfDay = to - from
val now = Calendar.getInstance() val now = Calendar.getInstance()
val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE) val d = (now.timeInMillis / (1000 * 60 * 60 * 24) -
Configuration.Schedule.referenceDate.time / (1000 * 60 * 60 * 24)).toInt()
val diff = day - d
val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE) + (diff * 60 * 24)
div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") { div("calendar-table") {
div("calendar-header") { attributes["data-day"] = day.toString()
div("calendar-cell") { attributes["data-reference"] = Configuration.Schedule.referenceDate.time.toString()
span { attributes["data-now"] = now.timeInMillis.toString()
+"Raum"
}
}
for (room in rooms) { div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") {
div("calendar-header") {
div("calendar-cell") { div("calendar-cell") {
attributes["data-room"] = room.id.toString()
span { span {
+room.name +"Raum"
} }
} }
}
}
div("calendar-body") {
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
val timeString = "$hours:$minutes"
val start = i * CALENDAR_GRID_WIDTH + from
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
var rowClass = "calendar-row"
if (currentTime in start..end) {
rowClass += " calendar-now calendar-now-${currentTime - start}"
}
div(rowClass) {
attributes["data-time"] = start.toString()
attributes["data-day"] = day.toString()
for (room in rooms) {
div("calendar-cell") { div("calendar-cell") {
if (time % gridLabelWidth == 0) { attributes["data-room"] = room.id.toString()
span {
+timeString span {
} +room.name
} }
} }
}
}
div("calendar-body") {
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
val timeString = "$hours:$minutes"
val start = i * CALENDAR_GRID_WIDTH + from
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
var rowClass = "calendar-row"
if (currentTime in start..end) {
rowClass += " calendar-now calendar-now-${currentTime - start}"
}
div(rowClass) {
attributes["data-time"] = start.toString()
attributes["data-day"] = day.toString()
for (room in rooms) {
div("calendar-cell") { div("calendar-cell") {
attributes["data-room"] = room.id.toString() if (time % gridLabelWidth == 0) {
span {
+timeString
}
}
}
title = room.name + " - " + timeString for (room in rooms) {
div("calendar-cell") {
attributes["data-room"] = room.id.toString()
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() title = room.name + " - " + timeString
calendarCell(schedule) val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule)
}
} }
} }
} }
@ -148,7 +160,7 @@ fun Route.calendar() {
val todayDay = todayDate.time / (1000 * 60 * 60 * 24) val todayDay = todayDate.time / (1000 * 60 * 60 * 24)
val day = todayDay - refDay val day = todayDay - refDay
call.respondRedirect("/calendar/$day", false) call.respondRedirect("$prefix/calendar/$day", false)
} }
get("/calendar/{day}/rtt") { get("/calendar/{day}/rtt") {
@ -159,7 +171,7 @@ fun Route.calendar() {
path = "/" path = "/"
) )
val day = call.parameters["day"]?.toIntOrNull() ?: 0 val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day") call.respondRedirect("$prefix/calendar/$day")
} }
get("/calendar/{day}/ttr") { get("/calendar/{day}/ttr") {
@ -170,7 +182,7 @@ fun Route.calendar() {
path = "/" path = "/"
) )
val day = call.parameters["day"]?.toIntOrNull() ?: 0 val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day") call.respondRedirect("$prefix/calendar/$day")
} }
get("/calendar/{day}") { get("/calendar/{day}") {
@ -237,20 +249,20 @@ fun Route.calendar() {
div("header") { div("header") {
div("header-left") { div("header-left") {
if (editable || day - 1 > range.start) { if (editable || day - 1 > range.start) {
a("/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } } a("$prefix/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } }
} }
span { span {
+dateString +dateString
} }
if (editable || day + 1 < range.endInclusive) { if (editable || day + 1 < range.endInclusive) {
a("/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } } a("$prefix/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } }
} }
} }
div("header-right") { div("header-right") {
a("/calendar/$day/rtt", classes = "form-btn") { a("$prefix/calendar/$day/rtt", classes = "form-btn") {
+"Vertikal" +"Vertikal"
} }
a("/calendar/$day/ttr", classes = "form-btn") { a("$prefix/calendar/$day/ttr", classes = "form-btn") {
+"Horizontal" +"Horizontal"
} }
if (editable) { if (editable) {
@ -267,19 +279,16 @@ fun Route.calendar() {
} }
div("calendar") { div("calendar") {
attributes["data-day"] = day.toString()
attributes["data-editable"] = editable.toString() attributes["data-editable"] = editable.toString()
div("calendar-table") { renderCalendar(
renderCalendar( orientation,
orientation, day,
day, min,
min, max,
max, rooms,
rooms, schedules
schedules )
)
}
if (editable) { if (editable) {
div("calendar-edit") { div("calendar-edit") {

View file

@ -16,6 +16,7 @@ import io.ktor.sessions.get
import io.ktor.sessions.sessions import io.ktor.sessions.sessions
import io.ktor.sessions.set import io.ktor.sessions.set
import kotlinx.html.* import kotlinx.html.*
import de.kif.backend.prefix
fun Route.login() { fun Route.login() {
route("login") { route("login") {
@ -24,11 +25,12 @@ fun Route.login() {
val principal = call.principal<UserPrinciple>() ?: return@post val principal = call.principal<UserPrinciple>() ?: return@post
if (principal.user.id == null) return@post if (principal.user.id == null) return@post
call.sessions.set(PortalSession(principal.user.id)) call.sessions.set(PortalSession(principal.user.id))
call.respondRedirect(call.parameters["redirect"] ?: "/") call.respondRedirect(call.parameters["redirect"] ?: "$prefix/")
} }
} }
get { get {
val redirect = call.parameters["redirect"] ?: "$prefix/"
val needLogin = call.sessions.get<PortalSession>() == null val needLogin = call.sessions.get<PortalSession>() == null
if (needLogin) { if (needLogin) {
respondMain { respondMain {
@ -36,7 +38,7 @@ fun Route.login() {
div { div {
div { div {
br { } br { }
form("/login", method = FormMethod.post) { form("$prefix/login?redirect=$redirect", method = FormMethod.post) {
div("form-group") { div("form-group") {
label { label {
htmlFor = "username" htmlFor = "username"
@ -68,7 +70,7 @@ fun Route.login() {
name = "redirect", name = "redirect",
type = InputType.hidden type = InputType.hidden
) { ) {
value = call.parameters["redirect"] ?: "/" value = redirect
} }
button(type = ButtonType.submit, classes = "form-btn btn-primary") { button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Einloggen" +"Einloggen"
@ -86,13 +88,13 @@ fun Route.login() {
} }
} }
} else { } else {
call.respondRedirect(call.parameters["redirect"] ?: "/") call.respondRedirect(redirect)
} }
} }
} }
get("logout") { get("logout") {
call.sessions.clear<PortalSession>() call.sessions.clear<PortalSession>()
call.respondRedirect("/") call.respondRedirect("$prefix/")
} }
} }

View file

@ -26,6 +26,7 @@ import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import kotlinx.html.* import kotlinx.html.*
import java.io.File import java.io.File
import de.kif.backend.prefix
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") { fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
var classes = "post" var classes = "post"
@ -39,11 +40,11 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str
attributes["data-id"] = post.id.toString() attributes["data-id"] = post.id.toString()
attributes["data-pinned"] = post.pinned.toString() attributes["data-pinned"] = post.pinned.toString()
a("/p/${post.url}", classes = "post-name") { a("$prefix/p/${post.url}", classes = "post-name") {
+post.name +post.name
} }
if (editable) { if (editable) {
a("/post/${post.id}", classes = "post-edit") { a("$prefix/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
@ -269,7 +270,7 @@ fun Route.overview() {
} }
div("form-group") { div("form-group") {
a("/") { a("$prefix/") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -279,7 +280,7 @@ fun Route.overview() {
} }
} }
} }
a("/post/${editPost.id}/delete") { a("$prefix/post/${editPost.id}/delete") {
button(classes = "form-btn btn-danger") { button(classes = "form-btn btn-danger") {
+"Löschen" +"Löschen"
} }
@ -360,7 +361,7 @@ fun Route.overview() {
PostRepository.update(post) PostRepository.update(post)
call.respondRedirect("/") call.respondRedirect("$prefix/")
} }
} }
@ -474,7 +475,7 @@ fun Route.overview() {
} }
div("form-group") { div("form-group") {
a("/") { a("$prefix/") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -536,7 +537,7 @@ fun Route.overview() {
PostRepository.create(post) PostRepository.create(post)
call.respondRedirect("/") call.respondRedirect("$prefix/")
} }
} }
@ -546,7 +547,7 @@ fun Route.overview() {
PostRepository.delete(postId) PostRepository.delete(postId)
call.respondRedirect("/") call.respondRedirect("$prefix/")
} }
} }
} }

View file

@ -24,6 +24,7 @@ import kotlinx.html.*
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
import de.kif.backend.prefix
fun Route.room() { fun Route.room() {
@ -38,7 +39,7 @@ fun Route.room() {
searchValue = search searchValue = search
action { action {
a("/room/new") { a("$prefix/room/new") {
button(classes = "form-btn btn-primary") { button(classes = "form-btn btn-primary") {
+"Raum hinzufügen" +"Raum hinzufügen"
} }
@ -92,7 +93,7 @@ fun Route.room() {
} }
} }
td(classes = "action") { td(classes = "action") {
a("/room/${u.id}") { a("$prefix/room/${u.id}") {
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
@ -223,7 +224,7 @@ fun Route.room() {
} }
div("form-group") { div("form-group") {
a("/room") { a("$prefix/room") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -233,7 +234,7 @@ fun Route.room() {
} }
} }
} }
a("/room/${editRoom.id}/delete") { a("$prefix/room/${editRoom.id}/delete") {
button(classes = "form-btn btn-danger") { button(classes = "form-btn btn-danger") {
+"Löschen" +"Löschen"
} }
@ -261,7 +262,7 @@ fun Route.room() {
RoomRepository.update(room) RoomRepository.update(room)
call.respondRedirect("/rooms") call.respondRedirect("$prefix/rooms")
} }
} }
@ -382,7 +383,7 @@ fun Route.room() {
} }
div("form-group") { div("form-group") {
a("/room") { a("$prefix/room") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -415,7 +416,7 @@ fun Route.room() {
RoomRepository.create(room) RoomRepository.create(room)
call.respondRedirect("/rooms") call.respondRedirect("$prefix/rooms")
} }
} }
@ -425,7 +426,7 @@ fun Route.room() {
RoomRepository.delete(roomId) RoomRepository.delete(roomId)
call.respondRedirect("/rooms") call.respondRedirect("$prefix/rooms")
} }
} }
} }

View file

@ -1,6 +1,5 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.TrackRepository
import de.kif.backend.view.MainTemplate import de.kif.backend.view.MainTemplate
@ -25,6 +24,7 @@ import kotlinx.css.Display
import kotlinx.html.* import kotlinx.html.*
import kotlin.collections.set import kotlin.collections.set
import kotlin.random.Random import kotlin.random.Random
import de.kif.backend.prefix
fun DIV.colorPicker(color: Color?) { fun DIV.colorPicker(color: Color?) {
val colorString = color?.toString() ?: Color( val colorString = color?.toString() ?: Color(
@ -95,7 +95,7 @@ fun Route.track() {
searchValue = search searchValue = search
action { action {
a("/track/new") { a("$prefix/track/new") {
button(classes = "form-btn btn-primary") { button(classes = "form-btn btn-primary") {
+"Track hinzufügen" +"Track hinzufügen"
} }
@ -128,7 +128,7 @@ fun Route.track() {
+u.color.toString() +u.color.toString()
} }
td(classes = "action") { td(classes = "action") {
a("/track/${u.id}") { a("$prefix/track/${u.id}") {
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
@ -168,7 +168,7 @@ fun Route.track() {
} }
div("form-group") { div("form-group") {
a("/track") { a("$prefix/track") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Cancel" +"Cancel"
} }
@ -178,7 +178,7 @@ fun Route.track() {
} }
} }
} }
a("/track/${editTrack.id}/delete") { a("$prefix/track/${editTrack.id}/delete") {
button(classes = "form-btn btn-danger") { button(classes = "form-btn btn-danger") {
+"Löschen" +"Löschen"
} }
@ -204,7 +204,7 @@ fun Route.track() {
TrackRepository.update(editTrack) TrackRepository.update(editTrack)
call.respondRedirect("/tracks") call.respondRedirect("$prefix/tracks")
} }
} }
@ -232,7 +232,7 @@ fun Route.track() {
colorPicker(null) colorPicker(null)
} }
div("form-group") { div("form-group") {
a("/track") { a("$prefix/track") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -262,7 +262,7 @@ fun Route.track() {
TrackRepository.create(track) TrackRepository.create(track)
call.respondRedirect("/tracks") call.respondRedirect("$prefix/tracks")
} }
} }
@ -272,7 +272,7 @@ fun Route.track() {
TrackRepository.delete(trackId) TrackRepository.delete(trackId)
call.respondRedirect("/tracks") call.respondRedirect("$prefix/tracks")
} }
} }
} }

View file

@ -1,11 +1,10 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.checkPassword
import de.kif.backend.hashPassword import de.kif.backend.hashPassword
import de.kif.backend.prefix
import de.kif.backend.repository.UserRepository import de.kif.backend.repository.UserRepository
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate import de.kif.backend.view.TableTemplate
import de.kif.backend.view.respondMain import de.kif.backend.view.respondMain
import de.kif.common.Search import de.kif.common.Search
@ -13,7 +12,6 @@ import de.kif.common.model.Permission
import de.kif.common.model.User import de.kif.common.model.User
import io.ktor.application.call import io.ktor.application.call
import io.ktor.html.insert import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.request.receiveParameters import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect import io.ktor.response.respondRedirect
import io.ktor.routing.Route import io.ktor.routing.Route
@ -38,7 +36,7 @@ fun Route.user() {
searchValue = search searchValue = search
action { action {
a("/user/new") { a("$prefix/user/new") {
button(classes = "form-btn btn-primary") { button(classes = "form-btn btn-primary") {
+"Nutzer hinzufügen" +"Nutzer hinzufügen"
} }
@ -71,7 +69,7 @@ fun Route.user() {
+u.permissions.joinToString(", ") { it.toString().toLowerCase() } +u.permissions.joinToString(", ") { it.toString().toLowerCase() }
} }
td(classes = "action") { td(classes = "action") {
a("/user/${u.id}") { a("$prefix/user/${u.id}") {
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
@ -131,17 +129,40 @@ fun Route.user() {
} }
div("form-group") { div("form-group") {
a("/user") { label {
button(classes = "form-btn") { htmlFor = "current-password"
+"Abbrechen" +"Passwort ändern"
}
} }
button(type = ButtonType.submit, classes = "form-btn btn-primary") { input(
+"Speichern" name = "current-password",
classes = "form-control",
type = InputType.password
) {
id = "current-password"
placeholder = "Aktuelles Passwort"
value = ""
}
input(
name = "new-password-1",
classes = "form-control",
type = InputType.password
) {
id = "new-password-1"
placeholder = "Neues Passwort"
value = ""
}
input(
name = "new-password-2",
classes = "form-control",
type = InputType.password
) {
id = "new-password-2"
placeholder = "Neues Passwort wiederholen"
value = ""
} }
} }
} }
a("/user/${editUser.id}/delete") { a("$prefix/user/${editUser.id}/delete") {
button(classes = "form-btn btn-danger") { button(classes = "form-btn btn-danger") {
+"Löschen" +"Löschen"
} }
@ -162,6 +183,16 @@ fun Route.user() {
params["username"]?.let { editUser = editUser.copy(username = it) } params["username"]?.let { editUser = editUser.copy(username = it) }
val currentPassword = params["current-password"]
val newPassword1 = params["new-password-1"]
val newPassword2 = params["new-password-2"]
if (currentPassword != null && newPassword1 != null && newPassword2 != null && currentPassword.isNotBlank() && newPassword1.isNotBlank() && newPassword2.isNotBlank()) {
if (checkPassword(currentPassword, editUser.password) && newPassword1 == newPassword2) {
editUser = editUser.copy(password = hashPassword(newPassword1))
}
}
val permissions = Permission.values().filter { permission -> val permissions = Permission.values().filter { permission ->
val name = permission.toString().toLowerCase() val name = permission.toString().toLowerCase()
user.checkPermission(permission) && params["permission-$name"] == "on" user.checkPermission(permission) && params["permission-$name"] == "on"
@ -170,7 +201,7 @@ fun Route.user() {
UserRepository.update(user) UserRepository.update(user)
call.respondRedirect("/users") call.respondRedirect("$prefix/users")
} }
} }
@ -235,7 +266,7 @@ fun Route.user() {
} }
div("form-group") { div("form-group") {
a("/user") { a("$prefix/user") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -270,7 +301,7 @@ fun Route.user() {
UserRepository.create(newUser) UserRepository.create(newUser)
call.respondRedirect("/users") call.respondRedirect("$prefix/users")
} }
} }
@ -285,7 +316,7 @@ fun Route.user() {
UserRepository.delete(userId) UserRepository.delete(userId)
} }
call.respondRedirect("/users") call.respondRedirect("$prefix/users")
} }
} }
} }

View file

@ -20,11 +20,12 @@ data class WallData(
suspend fun genWallData(day: Int): WallData { suspend fun genWallData(day: Int): WallData {
val list = ScheduleRepository.getByDay(day) val list = ScheduleRepository.getByDay(day)
val schedules = RoomRepository.all().associateWith { emptyMap<Int, Schedule>() } + list.groupBy { it.room }.mapValues { (_, it) -> val schedules =
it.associateBy { RoomRepository.all().associateWith { emptyMap<Int, Schedule>() } + list.groupBy { it.room }.mapValues { (_, it) ->
it.time it.associateBy {
it.time
}
} }
}
var max = 0 var max = 0
var min = 24 * 60 var min = 24 * 60
@ -59,16 +60,14 @@ fun Route.wall() {
for (day in days) { for (day in days) {
div("wall-box") { div("wall-box") {
div("wall-calendar calendar") { div("wall-calendar calendar") {
div("calendar-table") { renderCalendar(
renderCalendar( CalendarOrientation.TIME_TO_ROOM,
CalendarOrientation.TIME_TO_ROOM, day.number,
day.number, min,
min, max,
max, day.schedules.keys.toList().sortedBy { it.id },
day.schedules.keys.toList().sortedBy { it.id }, day.schedules
day.schedules )
)
}
} }
} }
} }

View file

@ -19,6 +19,7 @@ import kotlinx.css.CSSBuilder
import kotlinx.css.Display import kotlinx.css.Display
import kotlinx.html.* import kotlinx.html.*
import kotlin.collections.set import kotlin.collections.set
import de.kif.backend.prefix
private const val separator = "###" private const val separator = "###"
@ -33,12 +34,12 @@ fun Route.workGroup() {
searchValue = search searchValue = search
action { action {
a("/tracks") { a("$prefix/tracks") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Tracks bearbeiten" +"Tracks bearbeiten"
} }
} }
a("/workgroup/new") { a("$prefix/workgroup/new") {
button(classes = "form-btn btn-primary") { button(classes = "form-btn btn-primary") {
+"Arbeitskreis hinzufügen" +"Arbeitskreis hinzufügen"
} }
@ -132,7 +133,7 @@ fun Route.workGroup() {
} }
} }
td(classes = "action") { td(classes = "action") {
a("/workgroup/${u.id}") { a("$prefix/workgroup/${u.id}") {
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
@ -400,6 +401,8 @@ fun Route.workGroup() {
min = "-1337" min = "-1337"
max = "1337" max = "1337"
placeholder = "Tag"
} }
} }
ConstraintType.NotOnDay -> { ConstraintType.NotOnDay -> {
@ -415,6 +418,8 @@ fun Route.workGroup() {
min = "-1337" min = "-1337"
max = "1337" max = "1337"
placeholder = "Tag"
} }
} }
ConstraintType.OnlyBeforeTime -> { ConstraintType.OnlyBeforeTime -> {
@ -426,7 +431,8 @@ fun Route.workGroup() {
classes = "form-control" classes = "form-control"
) { ) {
value = constraint.day?.toString() ?: "" value = constraint.day?.toString() ?: ""
placeholder = "day"
placeholder = "Tag (optional)"
} }
input( input(
name = "constraint-only-before-time-$index", name = "constraint-only-before-time-$index",
@ -437,6 +443,8 @@ fun Route.workGroup() {
min = "-1337" min = "-1337"
max = "133700" max = "133700"
placeholder = "Minuten"
} }
} }
ConstraintType.OnlyAfterTime -> { ConstraintType.OnlyAfterTime -> {
@ -448,7 +456,8 @@ fun Route.workGroup() {
classes = "form-control" classes = "form-control"
) { ) {
value = constraint.day?.toString() ?: "" value = constraint.day?.toString() ?: ""
placeholder = "day"
placeholder = "Tag (optional)"
} }
input( input(
name = "constraint-only-after-time-$index", name = "constraint-only-after-time-$index",
@ -459,6 +468,8 @@ fun Route.workGroup() {
min = "-1337" min = "-1337"
max = "133700" max = "133700"
placeholder = "Minuten"
} }
} }
ConstraintType.NotAtSameTime -> { ConstraintType.NotAtSameTime -> {
@ -500,7 +511,7 @@ fun Route.workGroup() {
} }
div("form-group") { div("form-group") {
a("/workgroup") { a("$prefix/workgroup") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -510,7 +521,7 @@ fun Route.workGroup() {
} }
} }
} }
a("/workgroup/${editWorkGroup.id}/delete") { a("$prefix/workgroup/${editWorkGroup.id}/delete") {
button(classes = "form-btn btn-danger") { button(classes = "form-btn btn-danger") {
+"Löschen" +"Löschen"
} }
@ -558,7 +569,7 @@ fun Route.workGroup() {
WorkGroupRepository.update(editWorkGroup) WorkGroupRepository.update(editWorkGroup)
call.respondRedirect("/workgroups") call.respondRedirect("$prefix/workgroups")
} }
} }
@ -796,7 +807,7 @@ fun Route.workGroup() {
} }
div("form-group") { div("form-group") {
a("/workgroup") { a("$prefix/workgroup") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Abbrechen" +"Abbrechen"
} }
@ -856,7 +867,7 @@ fun Route.workGroup() {
WorkGroupRepository.create(workGroup) WorkGroupRepository.create(workGroup)
call.respondRedirect("/workgroups") call.respondRedirect("$prefix/workgroups")
} }
} }
@ -866,7 +877,7 @@ fun Route.workGroup() {
WorkGroupRepository.delete(workGroupId) WorkGroupRepository.delete(workGroupId)
call.respondRedirect("/workgroups") call.respondRedirect("$prefix/workgroups")
} }
} }
} }
@ -882,14 +893,14 @@ private fun parseConstraintParam(params: Map<String, String?>) = params.map { (k
} }
key.startsWith("constraint-only-after-time") -> { key.startsWith("constraint-only-after-time") -> {
if ("day" in key) { if ("day" in key) {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, day = it) } Constraint(ConstraintType.OnlyAfterTime, day = value?.toIntOrNull())
} else { } else {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, time = it) } value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, time = it) }
} }
} }
key.startsWith("constraint-only-before-time") -> { key.startsWith("constraint-only-before-time") -> {
if ("day" in key) { if ("day" in key) {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, day = it) } Constraint(ConstraintType.OnlyBeforeTime, day = value?.toIntOrNull())
} else { } else {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, time = it) } value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, time = it) }
} }

View file

@ -1,5 +1,6 @@
package de.kif.backend.util package de.kif.backend.util
import de.kif.backend.prefix
import de.kif.backend.repository.* import de.kif.backend.repository.*
import de.kif.common.* import de.kif.common.*
import de.kif.common.RepositoryType import de.kif.common.RepositoryType
@ -27,7 +28,7 @@ object PushService {
} }
fun Route.pushService() { fun Route.pushService() {
webSocket { webSocket("/") {
PushService.clients += this PushService.clients += this
try { try {

View file

@ -2,10 +2,14 @@ package de.kif.backend.view
import de.kif.backend.PortalSession import de.kif.backend.PortalSession
import de.kif.backend.Resources import de.kif.backend.Resources
import de.kif.backend.prefix
import de.kif.common.model.User import de.kif.common.model.User
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.html.* import io.ktor.html.Placeholder
import io.ktor.html.Template
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.request.path import io.ktor.request.path
import io.ktor.request.uri import io.ktor.request.uri
import io.ktor.response.respondRedirect import io.ktor.response.respondRedirect
@ -30,36 +34,41 @@ class MainTemplate(
title("KIF Portal") title("KIF Portal")
link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(href = "$prefix/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(
href = "$prefix/static/external/font/Montserrat.css",
type = LinkType.textCss,
rel = LinkRel.stylesheet
)
link( link(
href = "https://fonts.googleapis.com/css?family=Bungee|Oswald|Raleway", href = "https://fonts.googleapis.com/css?family=Bungee|Oswald|Raleway",
type = LinkType.textCss, type = LinkType.textCss,
rel = LinkRel.stylesheet rel = LinkRel.stylesheet
) )
link(href = "/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(href = "$prefix/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
when (theme) { when (theme) {
Theme.LIGHT -> { Theme.LIGHT -> {
// Ignore // Ignore
} }
Theme.DARK -> { Theme.DARK -> {
link(href = "/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(href = "$prefix/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
} }
Theme.PRINCESS -> { Theme.PRINCESS -> {
link(href = "/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(href = "$prefix/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
} }
Theme.BRETT -> { Theme.BRETT -> {
link(href = "/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(href = "$prefix/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
} }
} }
script(src = "/static/require.min.js") {} script(src = "$prefix/static/require.min.js") {}
script { script {
unsafe { unsafe {
+"require.config({baseUrl: '/static'});\n" +"let prefix = '$prefix';\n"
+("require([${Resources.jsModules}]);\n") +"require.config({baseUrl: '$prefix/static'});\n"
+"require([${Resources.jsModules}]);\n"
} }
} }
} }
@ -96,10 +105,10 @@ class MainTemplate(
} }
enum class Theme(val text: String, val display: Boolean, val dark: Boolean, val primaryColor: String) { enum class Theme(val text: String, val display: Boolean, val dark: Boolean, val primaryColor: String) {
LIGHT("Hell",true, false, "#B11D33"), LIGHT("Hell", true, false, "#B11D33"),
DARK("Dunkel",true, true, "#ef5350"), DARK("Dunkel", true, true, "#ef5350"),
PRINCESS("Barbie",true, false, "#B11D33"), PRINCESS("Barbie", true, false, "#B11D33"),
BRETT("Brett",false, false, "#B11D33"); BRETT("Brett", false, false, "#B11D33");
companion object { companion object {
private val lookup = values().toList().associateBy { it.name } private val lookup = values().toList().associateBy { it.name }
@ -118,7 +127,7 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
body: MainTemplate.(Theme) -> Unit body: MainTemplate.(Theme) -> Unit
) { ) {
val param = call.request.queryParameters["theme"] val param = call.request.queryParameters["theme"]
val url = call.request.uri.substring(1) val url = call.request.uri.substring(1 + prefix.length)
val user = call.sessions.get<PortalSession>()?.getUser(call) val user = call.sessions.get<PortalSession>()?.getUser(call)
if (param != null) { if (param != null) {
@ -126,7 +135,7 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
name = "theme", name = "theme",
value = Theme.lookup(param).name, value = Theme.lookup(param).name,
maxAge = Int.MAX_VALUE, maxAge = Int.MAX_VALUE,
path = "/" path = "$prefix/"
) )
call.respondRedirect(call.request.path()) call.respondRedirect(call.request.path())
} else { } else {

View file

@ -1,9 +1,11 @@
package de.kif.backend.view package de.kif.backend.view
import de.kif.backend.Configuration
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.User import de.kif.common.model.User
import io.ktor.html.Template import io.ktor.html.Template
import kotlinx.html.* import kotlinx.html.*
import de.kif.backend.prefix
class MenuTemplate( class MenuTemplate(
private val url: String, private val url: String,
@ -16,10 +18,10 @@ class MenuTemplate(
nav("menu") { nav("menu") {
div("container") { div("container") {
div("menu-left") { div("menu-left") {
a("/", classes = if (tab == null) "active" else null) { a("$prefix/", classes = if (tab == null) "active" else null) {
+"Neuigkeiten" +"Neuigkeiten"
} }
a("/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) { a("$prefix/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) {
+"Zeitplan" +"Zeitplan"
} }
} }
@ -30,26 +32,26 @@ class MenuTemplate(
val user = user val user = user
div("menu-content") { div("menu-content") {
if (user == null) { if (user == null) {
a("/account", classes = if (tab == Tab.LOGIN) "active" else null) { a("$prefix/account", classes = if (tab == Tab.LOGIN) "active" else null) {
+"Einloggen" +"Einloggen"
} }
} else { } else {
if (user.checkPermission(Permission.WORK_GROUP)) { if (user.checkPermission(Permission.WORK_GROUP)) {
a("/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) { a("$prefix/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) {
+"Arbeitskreise" +"Arbeitskreise"
} }
} }
if (user.checkPermission(Permission.ROOM)) { if (user.checkPermission(Permission.ROOM)) {
a("/rooms", classes = if (tab == Tab.ROOM) "active" else null) { a("$prefix/rooms", classes = if (tab == Tab.ROOM) "active" else null) {
+"Räume" +"Räume"
} }
} }
if (user.checkPermission(Permission.USER)) { if (user.checkPermission(Permission.USER)) {
a("/users", classes = if (tab == Tab.USER) "active" else null) { a("$prefix/users", classes = if (tab == Tab.USER) "active" else null) {
+"Nutzer" +"Nutzer"
} }
} }
a("/account", classes = if (tab == Tab.ACCOUNT) "active" else null) { a("$prefix/account", classes = if (tab == Tab.ACCOUNT) "active" else null) {
+user.username +user.username
} }
} }

View file

@ -4,10 +4,78 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIF Portal</title> <title>KIF Portal</title>
<link href="/static/external/material-icons.css" rel="Stylesheet" type="text/css">
<link href="/static/external/font/Montserrat.css" rel="Stylesheet" type="text/css"> <style>
<link href="https://fonts.googleapis.com/css?family=Bungee|Oswald" rel="Stylesheet" type="text/css"> :root {
<link href="/static/style/style.css" rel="Stylesheet" type="text/css"> --background-secondary-color: #f5f5f5;
--text-primary-color: #333;
}
.main-error {
width: 100%;
height: 4rem;
text-align: center;
margin-top: -2rem;
position: absolute;
top: 48%;
left: 0;
}
.main-error span {
display: block;
font-size: 1.2rem;
}
.main-error span:first-child {
font-size: 1.5rem;
padding-bottom: 0.4rem;
}
body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
margin: 0 auto;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
@media (min-width: 768px) {
.container {
width: 720px;
}
}
@media (min-width: 992px) {
.container {
width: 960px;
}
}
@media (min-width: 1200px) {
.container {
width: 1140px;
}
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View file

@ -4,10 +4,78 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIF Portal</title> <title>KIF Portal</title>
<link href="/static/external/material-icons.css" rel="Stylesheet" type="text/css">
<link href="/static/external/font/Montserrat.css" rel="Stylesheet" type="text/css"> <style>
<link href="https://fonts.googleapis.com/css?family=Bungee|Oswald" rel="Stylesheet" type="text/css"> :root {
<link href="/static/style/style.css" rel="Stylesheet" type="text/css"> --background-secondary-color: #f5f5f5;
--text-primary-color: #333;
}
.main-error {
width: 100%;
height: 4rem;
text-align: center;
margin-top: -2rem;
position: absolute;
top: 48%;
left: 0;
}
.main-error span {
display: block;
font-size: 1.2rem;
}
.main-error span:first-child {
font-size: 1.5rem;
padding-bottom: 0.4rem;
}
body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
margin: 0 auto;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
@media (min-width: 768px) {
.container {
width: 720px;
}
}
@media (min-width: 992px) {
.container {
width: 960px;
}
}
@media (min-width: 1200px) {
.container {
width: 1140px;
}
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View file

@ -4,10 +4,78 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIF Portal</title> <title>KIF Portal</title>
<link href="/static/external/material-icons.css" rel="Stylesheet" type="text/css">
<link href="/static/external/font/Montserrat.css" rel="Stylesheet" type="text/css"> <style>
<link href="https://fonts.googleapis.com/css?family=Bungee|Oswald" rel="Stylesheet" type="text/css"> :root {
<link href="/static/style/style.css" rel="Stylesheet" type="text/css"> --background-secondary-color: #f5f5f5;
--text-primary-color: #333;
}
.main-error {
width: 100%;
height: 4rem;
text-align: center;
margin-top: -2rem;
position: absolute;
top: 48%;
left: 0;
}
.main-error span {
display: block;
font-size: 1.2rem;
}
.main-error span:first-child {
font-size: 1.5rem;
padding-bottom: 0.4rem;
}
body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
margin: 0 auto;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
@media (min-width: 768px) {
.container {
width: 720px;
}
}
@media (min-width: 992px) {
.container {
width: 960px;
}
}
@media (min-width: 1200px) {
.container {
width: 1140px;
}
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View file

@ -1,6 +1,8 @@
[server] [server]
host = "localhost" host = "localhost"
port = 8080 port = 8080
debug = false
prefix = ""
[path] [path]
web = "web" web = "web"