diff --git a/portal.toml b/portal.toml index 24b7247..4c1c7d1 100644 --- a/portal.toml +++ b/portal.toml @@ -3,7 +3,7 @@ host = "localhost" port = 8080 [schedule] -reference = "2019-06-12" +reference = "2019-06-04" [general] wiki_url = "https://wiki.kif.rocks/w/index.php?title=KIF470:Arbeitskreise&action=raw" diff --git a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt index d3905c1..0931b08 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt @@ -31,7 +31,7 @@ data class Schedule( fun getAbsoluteEndTime(): Int = getAbsoluteStartTime() + workGroup.length companion object { - fun timeToString(time: Int): String { + fun timeOfDayToString(time: Int): String { var day = time % (24 * 60) if (day < 0) day += 24 * 60 @@ -40,5 +40,37 @@ data class Schedule( return hour.toString().padStart(2, '0') + ":" + minute.toString().padStart(2, '0') } + + fun timeDifferenceToString(time: Long): String { + if (time < 0) { + return "running" + } + + var remainder = time + + val seconds = (remainder % 60).toString().padStart(2, '0') + remainder /= 60 + + val minutes = (remainder % 60).toString().padStart(2, '0') + remainder /= 60 + + val hours = (remainder % 24).toString().padStart(2, '0') + remainder /= 24 + + val days = remainder + + if (days > 1) { + return "$days days" + } + if (days > 0) { + return "1 day" + } + + if (hours == "00") { + return "$minutes:$seconds" + } + + return "$hours:$minutes:$seconds" + } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index 7642969..ed186cf 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -1,5 +1,6 @@ package de.kif.frontend +import de.kif.frontend.views.board.initBoard import de.kif.frontend.views.calendar.initCalendar import de.kif.frontend.views.table.initTableLayout import de.kif.frontend.views.initWorkGroupConstraints @@ -30,4 +31,7 @@ fun main() = init { if (document.getElementsByClassName("post-edit-right").length > 0) { initPostEdit() } + if (document.getElementsByClassName("board").length > 0) { + initBoard() + } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt b/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt new file mode 100644 index 0000000..0763ba3 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt @@ -0,0 +1,29 @@ +package de.kif.frontend.views.board + +import de.kif.common.formatDateTime +import de.kif.frontend.iterator +import de.westermann.kwebview.interval +import org.w3c.dom.HTMLElement +import org.w3c.dom.get +import kotlin.browser.document +import kotlin.js.Date + +fun initBoard() { + val boardSchedules = document.getElementsByClassName("board-schedules")[0] as HTMLElement + val dateView = document.getElementsByClassName("board-header-date")[0] as HTMLElement + val referenceTime = boardSchedules.dataset["reference"]?.toLongOrNull() ?: 0L + + val scheduleList = mutableListOf() + + for (bs in boardSchedules.getElementsByClassName("board-schedule").iterator()) { + scheduleList += BoardSchedule(bs) + } + + interval(1000) { + val currentTime = Date.now().toLong() + dateView.innerText = formatDateTime(currentTime) + + val now = referenceTime - currentTime / 1000 + scheduleList.forEach { it.updateTime(now) } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/board/BoardSchedule.kt b/src/jsMain/kotlin/de/kif/frontend/views/board/BoardSchedule.kt new file mode 100644 index 0000000..1d3b3ef --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/board/BoardSchedule.kt @@ -0,0 +1,24 @@ +package de.kif.frontend.views.board + +import de.kif.common.model.Schedule +import de.kif.frontend.views.overview.getByClassOrCreate +import de.westermann.kwebview.View +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.get + +class BoardSchedule( + view: HTMLElement +) : View(view) { + private val roomView = view.getByClassOrCreate("board-schedule-room") + private val timeView = view.getByClassOrCreate("board-schedule-time") + private val colorView = view.getByClassOrCreate("board-schedule-color") + private val nameView = view.getByClassOrCreate("board-schedule-name") + + private var time: Long = timeView.dataset["time"]?.toLongOrNull() ?: 0L + + fun updateTime(now: Long) { + //console.log(time.toString(), now.toString()) + timeView.textContent = Schedule.timeDifferenceToString(time + now) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt index 4f6a085..6fd42d4 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt @@ -11,7 +11,7 @@ import de.westermann.kwebview.components.* class CalendarTools(entry: CalendarEntry) : ViewCollection() { fun setName(room: Room, time: Int) { - nameView.text = room.name + " - " + Schedule.timeToString(time) + nameView.text = room.name + " - " + Schedule.timeOfDayToString(time) } lateinit var schedule: Schedule diff --git a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt index 4cbf6ea..ced083b 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt @@ -70,9 +70,9 @@ fun async(timeout: Int = 1, block: () -> Unit): Int { return window.setTimeout(block, timeout) } -fun interval(timeout: Int, block: () -> Unit): Int { - if (timeout < 1) throw IllegalArgumentException("Timeout must be greater than 0!") - return window.setInterval(block, timeout) +fun interval(delay: Int, block: () -> Unit): Int { + if (delay < 1) throw IllegalArgumentException("Delay must be greater than 0!") + return window.setInterval(block, delay) } fun clearTimeout(id: Int) { diff --git a/src/jsMain/resources/style/_config.scss b/src/jsMain/resources/style/_config.scss new file mode 100644 index 0000000..3f17228 --- /dev/null +++ b/src/jsMain/resources/style/_config.scss @@ -0,0 +1,11 @@ +$border-radius: 0.2rem; +$transitionTime: 150ms; + +@mixin no-select() { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/src/jsMain/resources/style/components/_board.scss b/src/jsMain/resources/style/components/_board.scss new file mode 100644 index 0000000..4718443 --- /dev/null +++ b/src/jsMain/resources/style/components/_board.scss @@ -0,0 +1,73 @@ +@import "../config"; + +.board-header { + line-height: 6rem; + display: flex; + padding: 0 2rem; + + & > div { + flex-grow: 1; + font-family: "Bungee", sans-serif; + font-weight: normal; + font-size: 1.1rem; + text-align: center; + + &:first-child { + text-align: left; + } + &:last-child { + text-align: right; + } + } +} + +.board { + padding: 1rem 1rem 2rem; + display: flex; + overflow: hidden; + height: calc(100vh - 6rem); + + & > div { + flex-grow: 1; + flex-basis: 0; + } +} + +.board-schedule { + width: 100%; + position: relative; + + padding: 1rem 1rem; + + &:not(:last-child) { + border-bottom: solid 1px var(--table-border-color) + } +} + +.board-schedule-room { + display: block; + padding-left: 1rem; + line-height: 2rem; +} + +.board-schedule-time { + position: absolute; + top: 1rem; + right: 1rem; + line-height: 2rem; +} + +.board-schedule-color { + background-color: var(--primary-color); + position: absolute; + top: 3.25rem; + left: 1rem; + width: 0.5rem; + height: 1.5rem; +} + +.board-schedule-name { + display: block; + padding-left: 1rem; + line-height: 2rem; +} \ No newline at end of file diff --git a/src/jsMain/resources/style/components/_calendar.scss b/src/jsMain/resources/style/components/_calendar.scss new file mode 100644 index 0000000..ee6206a --- /dev/null +++ b/src/jsMain/resources/style/components/_calendar.scss @@ -0,0 +1,590 @@ +@import "../config"; + +.header { + height: 2.2rem; + line-height: 2.2rem; +} + +.header-left { + float: left; + + & > * { + display: block; + float: left; + } + + a { + i { + font-size: 1.8rem; + padding: 0 0.5rem; + position: relative; + } + + &:first-child i::after { + content: ''; + position: absolute; + left: 100%; + height: 100%; + width: 4rem; + } + + &:last-child i::after { + content: ''; + position: absolute; + height: 100%; + left: -4rem; + width: 4rem; + } + } +} + +.header-right { + float: right; + + &::after { + clear: both; + } +} + +.calendar { + width: 100%; + position: relative; + margin-top: 1rem; +} + +.calendar-table { + width: 100%; + overflow-x: scroll; + overflow-y: visible; + padding-bottom: 1rem; + position: relative; + transition: width $transitionTime; +} + +.calendar-edit { + width: 0; + height: 100%; + display: block; + position: absolute; + right: 0; + top: 0; + transition: width $transitionTime; + + .calendar-edit-main { + position: sticky; + opacity: 0; + transition: opacity $transitionTime, visibility $transitionTime; + top: 1rem; + visibility: hidden; + height: 100vh; + z-index: 2; + background-color: var(--background-secondary-color); + } +} + +.calendar-edit-search { + padding: 0.5rem; +} + +.calendar-edit-list { + height: calc(100% - 4.5rem); + overflow-x: hidden; + overflow-y: scroll; +} + +.calendar-work-group { + position: relative; + display: block; + border-radius: $border-radius; + line-height: 2rem; + font-size: 0.8rem; + white-space: nowrap; + text-overflow: ellipsis; + + background-color: var(--primary-color); + color: var(--primary-text-color); + + padding: 0 0.5rem; + margin: 0.5rem; + + &::after { + content: attr(data-language); + bottom: 0; + right: 1rem; + opacity: 0.6; + position: absolute; + text-transform: uppercase; + } + + &:hover .calendar-tools { + display: block; + } + + &.drag { + box-shadow: 0 0.1rem 0.2rem var(--shadow-color); + z-index: 2; + + .calendar-tools { + display: block; + } + } + + &.pending::before { + content: ''; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--background-primary-color); + opacity: 0.6; + } + + @include no-select() +} + +.calendar[data-editable = "true"].edit { + .calendar-table { + width: calc(100% - 16rem); + border-right: solid 1px var(--table-border-color); + } + + .calendar-edit { + width: 16rem; + } + + .calendar-edit-main { + opacity: 1; + visibility: visible; + } +} + +.calendar-tools { + position: absolute; + top: -5rem; + right: 0; + background-color: var(--background-primary-color); + border-radius: $border-radius; + display: none; + z-index: 10; + width: max-content; + + border: solid 1px var(--input-border-color); + box-shadow: 0 0.1rem 0.2rem var(--shadow-color); + + div { + clear: left; + + span { + float: left; + display: block; + width: 2rem; + line-height: 2rem; + height: 2rem; + text-align: center; + color: var(--icon-color-focused); + transition: color $transitionTime, background-color $transitionTime; + box-sizing: content-box; + + i { + font-size: 1rem; + } + + &:hover { + background: var(--table-header-color); + } + + &.disabled { + color: var(--icon-color-inactive); + } + + &:first-child { + padding-left: 0.5rem; + } + + &:last-child { + padding-right: 0.5rem; + } + } + + &:first-child { + span:first-child { + color: var(--text-primary-color); + width: 6rem; + + &:hover { + background-color: transparent; + } + } + + span { + padding-top: 0.2rem; + } + } + + &:last-child { + span { + padding-bottom: 0.2rem; + } + } + } + + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: -1rem; + height: 1rem; + } +} + +.calendar-entry { + position: absolute; + display: block; + border-radius: $border-radius; + z-index: 1; + line-height: 2rem; + font-size: 0.8rem; + white-space: nowrap; + text-overflow: ellipsis; + + background-color: var(--primary-color); + color: var(--primary-text-color); + + padding: 0 0.5rem; + + &::after { + content: attr(data-language); + bottom: 0; + right: 1rem; + opacity: 0.6; + position: absolute; + text-transform: uppercase; + } + + &:hover .calendar-tools { + display: block; + } + + &.drag { + box-shadow: 0 0.1rem 0.2rem var(--shadow-color); + z-index: 2; + + .calendar-tools { + display: block; + } + } + + &.pending::before { + content: ''; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--background-primary-color); + opacity: 0.6; + } + + &.error { + outline: solid 0.4rem var(--error-color); + } + + @include no-select() +} + +.calendar-table-box { + display: flex; + flex-wrap: nowrap; + flex-direction: column; + width: max-content; + height: max-content; + + .calendar-header, .calendar-row { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + width: max-content; + height: max-content; + position: relative; + } + + .calendar-row { + .calendar-cell:not(:first-child) { + &:hover { + background-color: var(--table-header-color); + } + } + } + + &.room-to-time { + .calendar-header { + line-height: 2rem; + height: 2rem; + width: 100%; + + .calendar-cell:first-child { + flex-grow: 1; + text-align: center; + } + + .calendar-cell:not(:first-child) { + width: 12rem; + position: relative; + padding-left: 0.2rem; + + &::before { + content: ''; + height: 100%; + left: 0; + top: 0; + border-left: solid 1px var(--table-border-color); + position: absolute; + } + } + } + + .calendar-row { + line-height: 2rem; + height: 1.3rem; + + .calendar-cell { + position: relative; + width: 12rem; + + &::before { + content: ''; + height: 100%; + width: 100%; + left: 0; + top: 0; + border-left: solid 1px var(--table-border-color); + position: absolute; + } + + .calendar-entry { + top: 0.1rem; + bottom: 0; + width: auto !important; + left: 0.1rem !important; + right: 0.1rem; + } + } + + .calendar-cell:first-child { + width: 6rem; + left: 0; + text-align: center; + } + + .calendar-cell:first-child::before { + border-left: none; + } + + &:nth-child(4n + 2) .calendar-cell::before { + border-top: solid 1px var(--table-border-color); + } + + &.calendar-now::before { + content: ''; + display: block; + position: absolute; + left: 6rem; + right: 0; + height: 1px; + border-bottom: solid 1px $primary-color; + } + + &.calendar-now::after { + content: ''; + display: block; + position: absolute; + left: 6rem; + transform: scale(1, 0.5) rotate(-45deg); + transform-origin: bottom; + border-bottom: solid 0.4rem $primary-color; + border-right: solid 0.4rem $primary-color; + border-top: solid 0.4rem transparent; + border-left: solid 0.4rem transparent; + margin-top: -0.55rem; + margin-left: -0.3rem; + } + + @for $i from 0 through 14 { + &.calendar-now-#{$i}::after, &.calendar-now-#{$i}::before { + top: ($i / 16) * 100% + } + } + } + } + + &.time-to-room { + flex-direction: row; + + .calendar-header, .calendar-row { + flex-direction: column; + line-height: 3rem; + + .calendar-cell { + height: 3rem; + } + } + + .calendar-header { + .calendar-cell { + position: relative; + width: 6rem; + + &:not(:first-child) { + border-top: solid 1px var(--table-border-color); + } + } + } + + .calendar-row { + .calendar-cell { + position: relative; + width: 1.5rem; + + &:not(:first-child) { + border-top: solid 1px var(--table-border-color); + } + + .calendar-entry { + top: 0.5rem !important; + bottom: 0.5rem; + width: 0; + left: 0; + height: auto !important; + } + + &:first-child { + font-size: 0.8rem; + position: relative; + padding-left: 0.2rem; + } + } + + &:nth-child(2n + 2) .calendar-cell::before { + content: ''; + height: 100%; + left: 0; + top: 0; + border-left: solid 1px var(--table-border-color); + position: absolute; + } + + &.calendar-now::before { + content: ''; + display: block; + position: absolute; + top: 3rem; + bottom: 0; + width: 1px; + border-right: solid 1px $primary-color; + } + + &.calendar-now::after { + content: ''; + display: block; + position: absolute; + top: 3rem; + transform: scale(0.5, 1) rotate(45deg); + transform-origin: right; + border-bottom: solid 0.4rem $primary-color; + border-right: solid 0.4rem $primary-color; + border-top: solid 0.4rem transparent; + border-left: solid 0.4rem transparent; + margin-top: -0.3rem; + margin-left: -0.55rem; + } + + @for $i from 0 through 14 { + &.calendar-now-#{$i}::after, &.calendar-now-#{$i}::before { + left: ($i / 16) * 100% + } + } + } + } +} + +.color-picker { + .color-picker-entry { + position: relative; + width: 4rem; + height: 4rem; + float: left; + + input { + visibility: hidden; + } + + label { + position: absolute; + top: 0.5rem; + left: 0.5rem; + width: 3rem; + height: 3rem; + border-radius: 1.5rem; + z-index: 1; + } + + input:checked ~ label::before { + content: ''; + position: absolute; + background-color: transparent; + top: 1rem; + left: 0.8rem; + width: 1.1rem; + height: 0.5rem; + transform: rotate(-45deg); + border-left: solid 0.3rem var(--background-primary-color); + border-bottom: solid 0.3rem var(--background-primary-color); + } + } + + .color-picker-custom { + position: relative; + clear: both; + height: 4rem; + + & > input { + visibility: hidden; + } + + label { + position: absolute; + top: 0.5rem; + left: 0.5rem; + width: 3rem; + height: 3rem; + border-radius: 1.5rem; + z-index: 1; + border: solid 0.1rem var(--text-primary-color); + } + + input:checked ~ label::before { + content: ''; + position: absolute; + background-color: transparent; + top: 1rem; + left: 0.8rem; + width: 1.1rem; + height: 0.5rem; + transform: rotate(-45deg); + border-left: solid 0.3rem var(--text-primary-color); + border-bottom: solid 0.3rem var(--text-primary-color); + } + + label input { + position: absolute; + left: 4rem; + top: 0.2rem; + height: 2rem; + width: 4rem; + } + } +} diff --git a/src/jsMain/resources/style/components/_form.scss b/src/jsMain/resources/style/components/_form.scss new file mode 100644 index 0000000..8ccf375 --- /dev/null +++ b/src/jsMain/resources/style/components/_form.scss @@ -0,0 +1,193 @@ +@import "../config"; + +.form-control { + border: solid 1px var(--input-border-color); + outline: none; + padding: 0 1rem; + line-height: 2.5rem; + height: 2.5rem; + width: 100%; + background-color: var(--background-primary-color); + border-radius: $border-radius; + margin: 1px; + transition: border-color $transitionTime; + color: var(--text-primary-color); + + &:focus { + border-color: var(--primary-color); + border-width: 2px; + } +} + +textarea.form-control { + height: 16rem; + line-height: 1.1rem; + padding: 0.6rem 1rem; +} + +select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 var(--text-primary-color); +} + +.form-group { + padding-bottom: 1rem; + display: block; + position: relative; + clear: both; + + label { + display: block; + padding-bottom: 0.3rem; + padding-left: 0.2rem; + } +} + +.form-switch { + line-height: 24px; + + input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + z-index: 0; + } + + label { + display: block; + padding: 0 0 0 44px; + cursor: pointer; + + &:before { + content: ''; + position: absolute; + top: 5px; + left: 0; + width: 36px; + height: 14px; + background-color: var(--bg-disabled-color); + border-radius: 14px; + z-index: 1; + transition: background-color 0.28s cubic-bezier(.4, 0, .2, 1); + } + + &:after { + content: ''; + position: absolute; + top: 2px; + left: 0; + width: 20px; + height: 20px; + background-color: var(--lever-disabled-color); + border-radius: 14px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); + z-index: 2; + transition: all 0.28s cubic-bezier(.4, 0, .2, 1); + transition-property: left, background-color; + } + + @include no-select + } + + input:checked + label { + &:before { + background-color: var(--bg-enabled-color); + } + + &:after { + left: 16px; + background-color: var(--lever-enabled-color); + } + } +} + +.form-switch-group { + padding: 0.5rem 0; +} + +.form-btn { + border: solid 1px var(--input-border-color); + outline: none; + padding: 0 1rem; + line-height: 2rem; + background-color: var(--background-primary-color); + color: var(--primary-color); + + display: inline-block; + margin-right: 0.6rem; + + border-radius: $border-radius; + font-weight: 600; + text-transform: uppercase; + font-size: 0.9rem; + + transition: border-color $transitionTime, outline-color $transitionTime; + + cursor: pointer; + + &:focus, &:hover { + border-color: var(--primary-color); + outline-color: var(--primary-color); + } +} + +.btn-primary { + background-color: var(--primary-color); + color: var(--primary-text-color); + border-color: var(--primary-color); +} + +.btn-danger { + background-color: var(--error-color); + color: var(--error-text-color); + border-color: var(--error-color); +} + +button::-moz-focus-inner, input::-moz-focus-inner, select::-moz-focus-inner { + border: 0; +} + +form { + margin-bottom: 2rem; + width: 100%; +} + +@media (min-width: 768px) { + form { + width: 24rem; + } +} + +.input-group { + display: flex; + + .form-btn { + height: 2.5rem; + line-height: 2.5rem; + margin-top: 1px; + } + + span { + white-space: nowrap; + } + + & > * { + margin-right: 0; + flex-grow: 1; + flex-shrink: 1; + + &:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-left: -1px; + } + + &:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } +} diff --git a/src/jsMain/resources/style/components/_menu.scss b/src/jsMain/resources/style/components/_menu.scss new file mode 100644 index 0000000..4227b6e --- /dev/null +++ b/src/jsMain/resources/style/components/_menu.scss @@ -0,0 +1,135 @@ +@import "../config"; + +.menu { + background-color: var(--background-secondary-color); + color: var(--text-primary-color); + width: 100%; + clear: both; + height: 6rem; + line-height: 6rem; + + a { + padding: 0 1rem; + color: var(--text-primary-color); + height: 100%; + display: inline-block; + font-family: "Bungee", sans-serif; + font-weight: normal; + font-size: 1.1rem; + position: relative; + text-transform: uppercase; + white-space: nowrap; + z-index: 6; + + &::after { + content: ''; + display: block; + position: absolute; + left: 1rem; + right: 1rem; + bottom: 1.8em; + height: 0.25rem; + + background: transparent; + transition: background-color $transitionTime; + } + + &.active::after { + background: var(--text-primary-color); + } + + &:hover { + background-color: var(--table-border-color); + + &::after { + background: var(--primary-color); + } + } + } + + & ~ * { + content: ''; + clear: both; + } +} + +.menu-left { + float: left; +} + +.menu-right { + float: right; + + .menu-icon { + display: block; + cursor: default; + + padding: 0 1rem; + color: var(--text-primary-color); + height: 100%; + font-size: 2rem; + position: relative; + } + + .menu-content { + display: none; + position: absolute; + background-color: var(--background-secondary-color); + z-index: 5; + left: 0; + right: 0; + + &::before { + content: ''; + top: -8rem; + left: 0; + right: 0; + height: 8rem; + display: block; + position: absolute; + } + + a { + display: block; + line-height: 3rem; + + &:after { + bottom: 0.55rem; + } + + &:last-child { + margin-bottom: 0.5rem; + } + } + } + + &:hover { + .menu-content { + display: block; + } + } +} + +@media (min-width: 992px) { + .menu-right { + .menu-icon { + display: none; + } + + .menu-content { + display: block; + position: static; + background-color: transparent; + z-index: unset; + + a { + display: inline-block; + line-height: unset; + + &:after { + bottom: 1.8em + } + } + } + } +} diff --git a/src/jsMain/resources/style/components/_overview.scss b/src/jsMain/resources/style/components/_overview.scss new file mode 100644 index 0000000..30d7a96 --- /dev/null +++ b/src/jsMain/resources/style/components/_overview.scss @@ -0,0 +1,223 @@ +@import "../config"; + +.overview { + display: flex; + flex-direction: column; +} + +.overview-main { + flex-grow: 4; + margin-right: 1rem; + width: 100%; +} + +.overview-side { + min-width: 20%; +} + +.overview-twitter { + height: 20rem; + background-color: #b3e6f9; +} + +.post { + position: relative; + padding-top: 2rem; + width: 100%; +} + +.post-name { + position: absolute; + top: 0; + left: 0; + font-size: 1.2rem; + color: var(--primary-color); + line-height: 2rem; + padding: 0 1rem; + + &:empty::before { + display: block; + content: 'No title'; + opacity: 0.5; + font-size: 1.2rem; + line-height: 2rem; + color: var(--text-primary-color); + } +} + +.post-edit { + position: absolute; + top: 0; + right: 1rem; + line-height: 2rem; +} + +.post-column { + display: flex; + flex-direction: column; +} + +.post-column-left, .post-column-right { + flex-grow: 1; +} + +.post-image { + width: 100%; + margin: 1rem 0 0; + padding-top: 75%; + background-position: center; + background-size: cover; +} + +.post-no-image .post-column-left { + display: none; +} + +.post-content { + padding: 0 1rem; + + h1, h2, h3, h4, h5, h6, p { + margin: 0.7rem 0; + padding: 0; + } + + h1 { + font-size: 1rem; + } + + h2 { + font-size: 1rem; + } + + h3 { + font-size: 1rem; + } + + h4 { + font-size: 1rem; + } + + h5 { + font-size: 1rem; + } + + h6 { + font-size: 1rem; + } + + table { + border-collapse: collapse; + border-spacing: 0; + } + + td { + border-top: solid 1px var(--table-border-color); + } + + th { + background-color: var(--table-header-color); + } + + td, th { + padding: 0.4rem; + } +} + +.post-footer { + color: var(--text-secondary-color); + padding: 0 1rem; + font-size: 0.8rem; + font-weight: 400; + margin-top: -0.3rem; +} + +.post-edit-container { + display: flex; + flex-direction: column; +} + +.post-edit-right { + margin-left: 0; +} + +.post-edit-image-box { + width: 100%; + display: flex; + margin-top: 0.4rem; + + & > div:nth-child(1) { + width: 20%; + } + + & > div:nth-child(2) { + width: 80%; + padding-left: 1rem; + } +} + +.post-edit-image { + width: 100%; + padding-top: 75%; + border: solid 1px var(--input-border-color); + margin: 0; + background-color: var(--background-primary-color); + background-position: center; + background-size: cover; +} + +.overview-post { + margin-bottom: 2rem; + + &::after { + content: ''; + position: absolute; + display: block; + bottom: -1rem; + height: 1px; + left: 0; + right: 0; + background: var(--table-border-color); + } +} + +@media (min-width: 768px) { + .post-column { + padding: 0 1rem; + } + + .overview-post::after { + left: 1rem; + right: 1rem; + } + + .post-content, .post-footer { + padding: 0; + } + + .overview-post { + .post-column { + flex-direction: row; + } + + .post-column-left { + width: 30%; + margin-right: 1rem; + } + + .post-column-right { + width: 70%; + } + } +} + +@media (min-width: 992px) { + .post-edit-container { + flex-direction: row; + } + .post-edit-right { + margin-left: 2rem; + } + .overview { + flex-direction: row; + } +} \ No newline at end of file diff --git a/src/jsMain/resources/style/components/_table-layout.scss b/src/jsMain/resources/style/components/_table-layout.scss new file mode 100644 index 0000000..3b9e16f --- /dev/null +++ b/src/jsMain/resources/style/components/_table-layout.scss @@ -0,0 +1,149 @@ +@import "../config"; + +.table-layout-search { + float: left; + padding-bottom: 0.5rem !important; +} + +.table-layout-action { + float: right; + line-height: 2.5rem; +} + +.table-layout-table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + clear: both; + + th { + text-align: start; + } + + th, td { + padding: 0 0.6rem; + } + + .action { + text-align: center; + } + + tr { + border-top: solid 1px var(--table-border-color); + + &:first-child { + background-color: var(--table-header-color); + height: 2.5rem; + line-height: 2.5rem; + } + + &:not(:first-child) { + height: 2rem; + line-height: 2rem; + + &:hover { + background-color: var(--table-header-color); + } + } + } + + span { + display: block; + position: relative; + + &:empty:before { + content: "\200b"; + } + + &.error { + background-color: var(--error-background-color); + } + } + + .table-select-box { + position: absolute; + z-index: 1; + background: var(--background-primary-color); + width: 10rem; + border: solid 1px var(--input-border-color); + border-radius: $border-radius; + padding: 0.5rem 0; + + span { + padding: 0 0.5rem; + + &:hover { + background-color: var(--table-header-color); + } + } + } +} + +.work-group-constraints { + position: relative; + + & > label { + margin-bottom: 0.8rem; + } + + .input-group { + margin-bottom: 0.5rem; + + span { + width: 4rem; + position: relative; + text-align: center; + overflow: hidden; + + &:hover::after { + content: 'DELETE'; + position: absolute; + top: 0; + left: 0; + width: 100%; + text-align: center; + + font-weight: bold; + color: var(--primary-text-color); + background: var(--primary-color); + } + } + + .form-control { + width: 12rem; + } + } +} + +.work-group-constraints-add { + position: absolute; + top: -0.5rem; + right: 0; +} + +.work-group-constraints-add-list { + position: absolute; + top: -2rem; + right: 0; + z-index: 1; + display: none; + padding: 0.5rem 0; + + background: var(--background-primary-color); + border: solid 1px var(--input-border-color); + border-radius: $border-radius; + + span { + padding: 0 0.5rem; + line-height: 2rem; + display: block; + + &:hover { + background-color: var(--table-header-color); + } + } + + &.active { + display: block; + } +} \ No newline at end of file diff --git a/src/jsMain/resources/style/princess.scss b/src/jsMain/resources/style/princess.scss new file mode 100644 index 0000000..28720dd --- /dev/null +++ b/src/jsMain/resources/style/princess.scss @@ -0,0 +1,31 @@ +@import "_color.scss"; + +$background-primary-color: #ffc3e1; +$background-secondary-color: #ffa5d2; + +$text-primary-color: #333; +$text-secondary-color: rgba($text-primary-color, 0.5); + +$primary-color: #B11D33; +$primary-text-color: #fff; + +$error-color: #F00; +$error-text-color: #fff; +$error-background-color: rgba($error-color, 0.5); + +$input-border-color: #888; +$table-border-color: rgba($text-primary-color, 0.1); +$table-header-color: rgba($text-primary-color, 0.06); + +$shadow-color: rgba($text-primary-color, 0.8); + +$icon-color-focused: rgba($text-primary-color, 0.87); +$icon-color: rgba($text-primary-color, 0.54); +$icon-color-inactive: rgba($text-primary-color, 0.38); + +$bg-disabled-color: rgba($text-primary-color, .26); +$bg-enabled-color: rgba($primary-color, .5); +$lever-disabled-color: $background-primary-color; +$lever-enabled-color: $primary-color; + +@include color-setting; diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 302839f..021ae19 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -1,19 +1,15 @@ -@import "_color.scss"; - -@mixin no-select() { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -$border-radius: 0.2rem; -$transitionTime: 150ms; +@import "color"; +@import "config"; @include color-setting; +@import "components/board"; +@import "components/calendar"; +@import "components/form"; +@import "components/menu"; +@import "components/overview"; +@import "components/table-layout"; + body, html { color: var(--text-primary-color); background: var(--background-secondary-color); @@ -56,6 +52,10 @@ a { margin: 0 auto; } +.container-full { + width: 100%; +} + * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -80,1228 +80,6 @@ a { } } -.menu { - background-color: var(--background-secondary-color); - color: var(--text-primary-color); - width: 100%; - clear: both; - height: 6rem; - line-height: 6rem; - - a { - padding: 0 1rem; - color: var(--text-primary-color); - height: 100%; - display: inline-block; - font-family: "Bungee", sans-serif; - font-weight: normal; - font-size: 1.1rem; - position: relative; - text-transform: uppercase; - white-space: nowrap; - z-index: 6; - - &::after { - content: ''; - display: block; - position: absolute; - left: 1rem; - right: 1rem; - bottom: 1.8em; - height: 0.25rem; - - background: transparent; - transition: background-color $transitionTime; - } - - &.active::after { - background: var(--text-primary-color); - } - - &:hover { - background-color: var(--table-border-color); - - &::after { - background: var(--primary-color); - } - } - } - - & ~ * { - content: ''; - clear: both; - } -} - -.menu-left { - float: left; -} - -.menu-right { - float: right; - - .menu-icon { - display: block; - cursor: default; - - padding: 0 1rem; - color: var(--text-primary-color); - height: 100%; - font-size: 2rem; - position: relative; - } - - .menu-content { - display: none; - position: absolute; - background-color: var(--background-secondary-color); - z-index: 5; - left: 0; - right: 0; - - &::before { - content: ''; - top: -8rem; - left: 0; - right: 0; - height: 8rem; - display: block; - position: absolute; - } - - a { - display: block; - line-height: 3rem; - - &:after { - bottom: 0.55rem; - } - - &:last-child { - margin-bottom: 0.5rem; - } - } - } - - &:hover { - .menu-content { - display: block; - } - } -} - -@media (min-width: 992px) { - .menu-right { - .menu-icon { - display: none; - } - - .menu-content { - display: block; - position: static; - background-color: transparent; - z-index: unset; - - a { - display: inline-block; - line-height: unset; - - &:after { - bottom: 1.8em - } - } - } - } -} - -.main { - //padding: 0 1rem; -} - -.table-layout-search { - float: left; - padding-bottom: 0.5rem !important; -} - -.table-layout-action { - float: right; - line-height: 2.5rem; -} - -.table-layout-table { - width: 100%; - border-spacing: 0; - border-collapse: collapse; - clear: both; - - th { - text-align: start; - } - - th, td { - padding: 0 0.6rem; - } - - .action { - text-align: center; - } - - tr { - border-top: solid 1px var(--table-border-color); - - &:first-child { - background-color: var(--table-header-color); - height: 2.5rem; - line-height: 2.5rem; - } - - &:not(:first-child) { - height: 2rem; - line-height: 2rem; - - &:hover { - background-color: var(--table-header-color); - } - } - } - - span { - display: block; - position: relative; - - &:empty:before { - content: "\200b"; - } - - &.error { - background-color: var(--error-background-color); - } - } - - .table-select-box { - position: absolute; - z-index: 1; - background: var(--background-primary-color); - width: 10rem; - border: solid 1px var(--input-border-color); - border-radius: $border-radius; - padding: 0.5rem 0; - - span { - padding: 0 0.5rem; - - &:hover { - background-color: var(--table-header-color); - } - } - } -} - -.form-control { - border: solid 1px var(--input-border-color); - outline: none; - padding: 0 1rem; - line-height: 2.5rem; - height: 2.5rem; - width: 100%; - background-color: var(--background-primary-color); - border-radius: $border-radius; - margin: 1px; - transition: border-color $transitionTime; - color: var(--text-primary-color); - - &:focus { - border-color: var(--primary-color); - border-width: 2px; - } -} - -textarea.form-control { - height: 16rem; - line-height: 1.1rem; - padding: 0.6rem 1rem; -} - -select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 var(--text-primary-color); -} - -.form-group { - padding-bottom: 1rem; - display: block; - position: relative; - clear: both; - - label { - display: block; - padding-bottom: 0.3rem; - padding-left: 0.2rem; - } -} - -.form-switch { - line-height: 24px; - - input { - position: absolute; - top: 0; - left: 0; - width: 0; - height: 0; - opacity: 0; - z-index: 0; - } - - label { - display: block; - padding: 0 0 0 44px; - cursor: pointer; - - &:before { - content: ''; - position: absolute; - top: 5px; - left: 0; - width: 36px; - height: 14px; - background-color: var(--bg-disabled-color); - border-radius: 14px; - z-index: 1; - transition: background-color 0.28s cubic-bezier(.4, 0, .2, 1); - } - - &:after { - content: ''; - position: absolute; - top: 2px; - left: 0; - width: 20px; - height: 20px; - background-color: var(--lever-disabled-color); - border-radius: 14px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); - z-index: 2; - transition: all 0.28s cubic-bezier(.4, 0, .2, 1); - transition-property: left, background-color; - } - - @include no-select - } - - input:checked + label { - &:before { - background-color: var(--bg-enabled-color); - } - - &:after { - left: 16px; - background-color: var(--lever-enabled-color); - } - } -} - -.form-switch-group { - padding: 0.5rem 0; -} - -.form-btn { - border: solid 1px var(--input-border-color); - outline: none; - padding: 0 1rem; - line-height: 2rem; - background-color: var(--background-primary-color); - color: var(--primary-color); - - display: inline-block; - margin-right: 0.6rem; - - border-radius: $border-radius; - font-weight: 600; - text-transform: uppercase; - font-size: 0.9rem; - - transition: border-color $transitionTime, outline-color $transitionTime; - - cursor: pointer; - - &:focus, &:hover { - border-color: var(--primary-color); - outline-color: var(--primary-color); - } -} - -.btn-primary { - background-color: var(--primary-color); - color: var(--primary-text-color); - border-color: var(--primary-color); -} - -.btn-danger { - background-color: var(--error-color); - color: var(--error-text-color); - border-color: var(--error-color); -} - -button::-moz-focus-inner, input::-moz-focus-inner, select::-moz-focus-inner { - border: 0; -} - -form { - margin-bottom: 2rem; - width: 100%; -} - -@media (min-width: 768px) { - form { - width: 24rem; - } -} - -.input-group { - display: flex; - - .form-btn { - height: 2.5rem; - line-height: 2.5rem; - margin-top: 1px; - } - - span { - white-space: nowrap; - } - - & > * { - margin-right: 0; - flex-grow: 1; - flex-shrink: 1; - - &:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - margin-left: -1px; - } - - &:not(:last-child) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - } -} - -.header { - height: 2.2rem; - line-height: 2.2rem; -} - -.header-left { - float: left; - - & > * { - display: block; - float: left; - } - - a { - i { - font-size: 1.8rem; - padding: 0 0.5rem; - position: relative; - } - - &:first-child i::after { - content: ''; - position: absolute; - left: 100%; - height: 100%; - width: 4rem; - } - - &:last-child i::after { - content: ''; - position: absolute; - height: 100%; - left: -4rem; - width: 4rem; - } - } -} - -.header-right { - float: right; - - &::after { - clear: both; - } -} - -.calendar { - width: 100%; - position: relative; - margin-top: 1rem; -} - -.calendar-table { - width: 100%; - overflow-x: scroll; - overflow-y: visible; - padding-bottom: 1rem; - position: relative; - transition: width $transitionTime; -} - -.calendar-edit { - width: 0; - height: 100%; - display: block; - position: absolute; - right: 0; - top: 0; - transition: width $transitionTime; - - .calendar-edit-main { - position: sticky; - opacity: 0; - transition: opacity $transitionTime, visibility $transitionTime; - top: 1rem; - visibility: hidden; - height: 100vh; - } -} - -.calendar-edit-search { - padding: 0.5rem; -} - -.calendar-edit-list { - height: calc(100% - 4.5rem); - overflow-x: hidden; - overflow-y: scroll; -} - -.calendar-work-group { - position: relative; - display: block; - border-radius: $border-radius; - line-height: 2rem; - font-size: 0.8rem; - white-space: nowrap; - text-overflow: ellipsis; - - background-color: var(--primary-color); - color: var(--primary-text-color); - - padding: 0 0.5rem; - margin: 0.5rem; - - &::after { - content: attr(data-language); - bottom: 0; - right: 1rem; - opacity: 0.6; - position: absolute; - text-transform: uppercase; - } - - &:hover .calendar-tools { - display: block; - } - - &.drag { - box-shadow: 0 0.1rem 0.2rem var(--shadow-color); - z-index: 2; - - .calendar-tools { - display: block; - } - } - - &.pending::before { - content: ''; - display: block; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: var(--background-primary-color); - opacity: 0.6; - } - - @include no-select() -} - -.calendar[data-editable = "true"].edit { - .calendar-table { - width: calc(100% - 16rem); - border-right: solid 1px var(--table-border-color); - } - - .calendar-edit { - width: 16rem; - } - - .calendar-edit-main { - opacity: 1; - visibility: visible; - } -} - -.calendar-tools { - position: absolute; - top: -5rem; - right: 0; - background-color: var(--background-primary-color); - border-radius: $border-radius; - display: none; - z-index: 10; - width: max-content; - - border: solid 1px var(--input-border-color); - box-shadow: 0 0.1rem 0.2rem var(--shadow-color); - - div { - clear: left; - - span { - float: left; - display: block; - width: 2rem; - line-height: 2rem; - height: 2rem; - text-align: center; - color: var(--icon-color-focused); - transition: color $transitionTime, background-color $transitionTime; - box-sizing: content-box; - - i { - font-size: 1rem; - } - - &:hover { - background: var(--table-header-color); - } - - &.disabled { - color: var(--icon-color-inactive); - } - - &:first-child { - padding-left: 0.5rem; - } - &:last-child { - padding-right: 0.5rem; - } - } - - &:first-child { - span:first-child { - color: var(--text-primary-color); - width: 6rem; - - &:hover { - background-color: transparent; - } - } - - span { - padding-top: 0.2rem; - } - } - - &:last-child { - span { - padding-bottom: 0.2rem; - } - } - } - - &::after { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: -1rem; - height: 1rem; - } -} - -.calendar-entry { - position: absolute; - display: block; - border-radius: $border-radius; - z-index: 1; - line-height: 2rem; - font-size: 0.8rem; - white-space: nowrap; - text-overflow: ellipsis; - - background-color: var(--primary-color); - color: var(--primary-text-color); - - padding: 0 0.5rem; - - &::after { - content: attr(data-language); - bottom: 0; - right: 1rem; - opacity: 0.6; - position: absolute; - text-transform: uppercase; - } - - &:hover .calendar-tools { - display: block; - } - - &.drag { - box-shadow: 0 0.1rem 0.2rem var(--shadow-color); - z-index: 2; - - .calendar-tools { - display: block; - } - } - - &.pending::before { - content: ''; - display: block; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: var(--background-primary-color); - opacity: 0.6; - } - - &.error { - outline: solid 0.4rem var(--error-color); - } - - @include no-select() -} - -.calendar-table-box { - display: flex; - flex-wrap: nowrap; - flex-direction: column; - width: max-content; - height: max-content; - - .calendar-header, .calendar-row { - display: flex; - flex-wrap: nowrap; - flex-direction: row; - width: max-content; - height: max-content; - } - - .calendar-row { - .calendar-cell { - &:hover { - background-color: var(--table-header-color); - } - } - } - - &.room-to-time { - .calendar-header { - line-height: 2rem; - height: 2rem; - width: 100%; - - .calendar-cell:first-child { - flex-grow: 1; - text-align: center; - } - - .calendar-cell:not(:first-child) { - width: 12rem; - position: relative; - padding-left: 0.2rem; - - &::before { - content: ''; - height: 100%; - left: 0; - top: 0; - border-left: solid 1px var(--table-border-color); - position: absolute; - } - } - } - - .calendar-row { - line-height: 2rem; - height: 1.3rem; - - .calendar-cell { - position: relative; - width: 12rem; - - &::before { - content: ''; - height: 100%; - width: 100%; - left: 0; - top: 0; - border-left: solid 1px var(--table-border-color); - position: absolute; - } - - .calendar-entry { - top: 0.1rem; - bottom: 0; - width: auto !important; - left: 0.1rem !important; - right: 0.1rem; - } - } - - .calendar-cell:first-child { - width: 6rem; - left: 0; - text-align: center; - } - - .calendar-cell:first-child::before { - border-left: none; - } - - &:nth-child(4n + 2) .calendar-cell::before { - border-top: solid 1px var(--table-border-color); - } - } - } - - &.time-to-room { - flex-direction: row; - - .calendar-header, .calendar-row { - flex-direction: column; - line-height: 3rem; - - .calendar-cell { - height: 3rem; - } - } - - .calendar-header { - .calendar-cell { - position: relative; - width: 6rem; - - &:not(:first-child) { - border-top: solid 1px var(--table-border-color); - } - } - } - - .calendar-row { - .calendar-cell { - position: relative; - width: 1.5rem; - - &:not(:first-child) { - border-top: solid 1px var(--table-border-color); - } - .calendar-entry { - top: 0.5rem !important; - bottom: 0.5rem; - width: 0; - left: 0; - height: auto !important; - } - - &:first-child { - font-size: 0.8rem; - position: relative; - padding-left: 0.2rem; - } - } - - &:nth-child(2n + 2) .calendar-cell::before { - content: ''; - height: 100%; - left: 0; - top: 0; - border-left: solid 1px var(--table-border-color); - position: absolute; - } - } - } -} - -.color-picker { - .color-picker-entry { - position: relative; - width: 4rem; - height: 4rem; - float: left; - - input { - visibility: hidden; - } - - label { - position: absolute; - top: 0.5rem; - left: 0.5rem; - width: 3rem; - height: 3rem; - border-radius: 1.5rem; - z-index: 1; - } - - input:checked ~ label::before { - content: ''; - position: absolute; - background-color: transparent; - top: 1rem; - left: 0.8rem; - width: 1.1rem; - height: 0.5rem; - transform: rotate(-45deg); - border-left: solid 0.3rem var(--background-primary-color); - border-bottom: solid 0.3rem var(--background-primary-color); - } - } - - .color-picker-custom { - position: relative; - clear: both; - height: 4rem; - - & > input { - visibility: hidden; - } - - label { - position: absolute; - top: 0.5rem; - left: 0.5rem; - width: 3rem; - height: 3rem; - border-radius: 1.5rem; - z-index: 1; - border: solid 0.1rem var(--text-primary-color); - } - - input:checked ~ label::before { - content: ''; - position: absolute; - background-color: transparent; - top: 1rem; - left: 0.8rem; - width: 1.1rem; - height: 0.5rem; - transform: rotate(-45deg); - border-left: solid 0.3rem var(--text-primary-color); - border-bottom: solid 0.3rem var(--text-primary-color); - } - - label input { - position: absolute; - left: 4rem; - top: 0.2rem; - height: 2rem; - width: 4rem; - } - } -} - -.work-group-constraints { - position: relative; - - & > label { - margin-bottom: 0.8rem; - } - - .input-group { - margin-bottom: 0.5rem; - - span { - width: 4rem; - position: relative; - text-align: center; - overflow: hidden; - - &:hover::after { - content: 'DELETE'; - position: absolute; - top: 0; - left: 0; - width: 100%; - text-align: center; - - font-weight: bold; - color: var(--primary-text-color); - background: var(--primary-color); - } - } - - .form-control { - width: 12rem; - } - } -} - -.work-group-constraints-add { - position: absolute; - top: -0.5rem; - right: 0; -} - -.work-group-constraints-add-list { - position: absolute; - top: -2rem; - right: 0; - z-index: 1; - display: none; - padding: 0.5rem 0; - - background: var(--background-primary-color); - border: solid 1px var(--input-border-color); - border-radius: $border-radius; - - span { - padding: 0 0.5rem; - line-height: 2rem; - display: block; - - &:hover { - background-color: var(--table-header-color); - } - } - - &.active { - display: block; - } -} - -.overview { - display: flex; - flex-direction: column; -} - -.overview-main { - flex-grow: 4; - margin-right: 1rem; - width: 100%; -} - -.overview-side { - min-width: 20%; -} - -.overview-twitter { - height: 20rem; - background-color: #b3e6f9; -} - -.post { - position: relative; - padding-top: 2rem; - width: 100%; -} - -.post-name { - position: absolute; - top: 0; - left: 0; - font-size: 1.2rem; - color: var(--primary-color); - line-height: 2rem; - padding: 0 1rem; - - &:empty::before { - display: block; - content: 'No title'; - opacity: 0.5; - font-size: 1.2rem; - line-height: 2rem; - color: var(--text-primary-color); - } -} - -.post-edit { - position: absolute; - top: 0; - right: 1rem; - line-height: 2rem; -} - -.post-column { - display: flex; - flex-direction: column; -} - -.post-column-left, .post-column-right { - flex-grow: 1; -} - -.post-image { - width: 100%; - margin: 0.4rem 0 0; - padding-top: 75%; - background-position: center; - background-size: cover; -} - -.post-no-image .post-column-left { - display: none; -} - -.post-content { - padding: 0 1rem; - - h1, h2, h3, h4, h5, h6, p { - margin: 0.7rem 0; - padding: 0; - } - - h1 { - font-size: 1rem; - } - - h2 { - font-size: 1rem; - } - - h3 { - font-size: 1rem; - } - - h4 { - font-size: 1rem; - } - - h5 { - font-size: 1rem; - } - - h6 { - font-size: 1rem; - } - - table { - border-collapse: collapse; - border-spacing: 0; - } - - td { - border-top: solid 1px var(--table-border-color); - } - - th { - background-color: var(--table-header-color); - } - - td, th { - padding: 0.4rem; - } -} - -.post-footer { - color: var(--text-secondary-color); - padding: 0 1rem; - font-size: 0.8rem; - font-weight: 400; - margin-top: -0.3rem; -} - -.post-edit-container { - display: flex; - flex-direction: column; -} - -.post-edit-right { - margin-left: 0; -} - -.post-edit-image-box { - width: 100%; - display: flex; - margin-top: 0.4rem; - - & > div:nth-child(1) { - width: 20%; - } - - & > div:nth-child(2) { - width: 80%; - padding-left: 1rem; - } -} - -.post-edit-image { - width: 100%; - padding-top: 75%; - border: solid 1px var(--input-border-color); - margin: 0; - background-color: var(--background-primary-color); - background-position: center; - background-size: cover; -} - -.overview-post { - margin-bottom: 2rem; - - &::after { - content: ''; - position: absolute; - display: block; - bottom: -1rem; - height: 1px; - left: 0; - right: 0; - background: var(--table-border-color); - } -} - -@media (min-width: 768px) { - .post-column { - padding: 0 1rem; - } - - .overview-post::after { - left: 1rem; - right: 1rem; - } - - .post-content, .post-footer { - padding: 0; - } - - .overview-post { - .post-column { - flex-direction: row; - } - - .post-column-left { - width: 30%; - margin-right: 1rem; - } - - .post-column-right { - width: 70%; - } - } -} - -@media (min-width: 992px) { - .post-edit-container { - flex-direction: row; - } - .post-edit-right { - margin-left: 2rem; - } - .overview { - flex-direction: row; - } -} - .footer { height: 5rem; } @@ -1332,6 +110,10 @@ form { border: solid 0.2rem transparent; } + &.selected { + display: none; + } + &.selected::after { border-color: var(--primary-color) !important; } @@ -1346,4 +128,28 @@ form { background-color: #2d2d2d; border-color: #2d2d2d; } -} \ No newline at end of file + + #theme-princess::after { + background-color: #ffa5d2; + border-color: #ffa5d2; + } +} + +.main-error { + width: 100%; + height: 4rem; + text-align: center; + margin-top: -2rem; + position: absolute; + top: 48%; + + span { + display: block; + font-size: 1.2rem; + + &:first-child { + font-size: 1.5rem; + padding-bottom: 0.4rem; + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index 7affb55..c48c23e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -5,11 +5,14 @@ import de.kif.backend.route.* import de.kif.backend.route.api.* import de.kif.backend.util.pushService import io.ktor.application.Application +import io.ktor.application.call import io.ktor.application.install import io.ktor.features.* +import io.ktor.http.HttpStatusCode import io.ktor.http.content.files import io.ktor.http.content.static import io.ktor.jackson.jackson +import io.ktor.response.respond import io.ktor.routing.routing import io.ktor.websocket.WebSockets @@ -21,6 +24,14 @@ fun Application.main() { install(DataConversion) install(WebSockets) + install(StatusPages) { + exception { + it.printStackTrace() + call.respond(HttpStatusCode.InternalServerError) + } + statusFile(HttpStatusCode.NotFound, HttpStatusCode.InternalServerError, filePattern = "error#.html") + } + install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) @@ -44,6 +55,8 @@ fun Application.main() { login() account() + board() + workGroup() track() room() diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt new file mode 100644 index 0000000..59e9f5b --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt @@ -0,0 +1,82 @@ +package de.kif.backend.route + +import de.kif.backend.Configuration +import de.kif.backend.repository.PostRepository +import de.kif.backend.repository.ScheduleRepository +import de.kif.backend.view.respondMain +import de.kif.common.formatDateTime +import de.kif.common.model.Schedule +import io.ktor.routing.Route +import io.ktor.routing.get +import kotlinx.css.CSSBuilder +import kotlinx.css.Color +import kotlinx.html.div +import kotlinx.html.span +import java.util.* + +fun Route.board() { + get("/board") { + val postList = PostRepository.all().asReversed() + val scheduleList = ScheduleRepository.all().map { + it to it.getAbsoluteStartTime() * 60 + }.sortedBy { it.second } + + val referenceTime = Configuration.Schedule.referenceDate.time / 1000 + val now = referenceTime - (Date().time / 1000) + + respondMain(true, true) { + content { + div("board-header") { + div { + +"KIF 47.0" + } + div("board-header-date") { + +formatDateTime(Date().time) + } + } + div("board") { + div("board-schedules") { + attributes["data-reference"] = referenceTime.toString() + + for ((schedule, time) in scheduleList) { + div("board-schedule") { + attributes["data-id"] = schedule.id.toString() + + span("board-schedule-room") { + +schedule.room.name + } + span("board-schedule-time") { + attributes["data-time"] = time.toString() + attributes["data-duration"] = schedule.workGroup.length.toString() + + +Schedule.timeDifferenceToString(time + now) + } + span("board-schedule-color") { + attributes["style"] = CSSBuilder().apply { + val c = schedule.workGroup.track?.color + if (c != null) { + backgroundColor = Color(c.toString()) + } + }.toString() + } + span("board-schedule-name") { + +schedule.workGroup.name + } + } + } + } + + div("board-posts") { + for (post in postList) { + createPost(post, false, "board-post overview-post") + } + } + + div("board-twitter") { + + } + } + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index bbccc35..c931c85 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -21,6 +21,7 @@ import kotlinx.css.Color import kotlinx.css.pct import kotlinx.css.rem import kotlinx.html.* +import java.util.* import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set @@ -67,6 +68,9 @@ private fun DIV.renderCalendar( val gridLabelWidth = 60 val minutesOfDay = to - from + val now = Calendar.getInstance() + val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE) + div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") { div("calendar-header") { div("calendar-cell") { @@ -95,7 +99,14 @@ private fun DIV.renderCalendar( val start = i * CALENDAR_GRID_WIDTH + from val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1 - div("calendar-row") { + var rowClass = "calendar-row" + + if (currentTime in start..end) { + rowClass += " calendar-now calendar-now-${currentTime - start}" + } + + div(rowClass) { + div("calendar-cell") { if (time % gridLabelWidth == 0) { span { diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt index 8babe11..7f1b378 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt @@ -9,7 +9,11 @@ import io.ktor.response.respondRedirect import io.ktor.util.pipeline.PipelineContext import kotlinx.html.* -class MainTemplate(private val theme: Theme) : Template { +class MainTemplate( + private val theme: Theme, + private val noMenu: Boolean, + private val stretch: Boolean +) : Template { val content = Placeholder() val menuTemplate = TemplatePlaceholder() @@ -36,6 +40,9 @@ class MainTemplate(private val theme: Theme) : Template { Theme.DARK -> { link(href = "/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet) } + Theme.PRINCESS -> { + link(href = "/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + } } script(src = "/static/require.min.js") {} @@ -48,23 +55,29 @@ class MainTemplate(private val theme: Theme) : Template { } } body { - insert(MenuTemplate(), menuTemplate) - div("container") { + if (!noMenu) { + insert(MenuTemplate(), menuTemplate) + } + + val containerClasses = if (stretch) "container-full" else "container" + div(containerClasses) { div("main") { insert(content) } } - div("footer") { - div("container") { - div("footer-credit") { - } - div("footer-theme") { - for (it in Theme.values()) { - val name = it.name.toLowerCase() - a("?theme=${it.name}", classes = if (theme == it) "selected" else "") { - id = "theme-$name" - +name.capitalize() + if (!noMenu) { + div("footer") { + div("container") { + div("footer-credit") { + } + div("footer-theme") { + for (it in Theme.values()) { + val name = it.name.toLowerCase() + a("?theme=${it.name}", classes = if (theme == it) "selected" else "") { + id = "theme-$name" + +name.capitalize() + } } } } @@ -75,14 +88,18 @@ class MainTemplate(private val theme: Theme) : Template { } enum class Theme { - LIGHT, DARK + LIGHT, DARK, PRINCESS } private fun String?.toTheme() = this?.let { str -> Theme.values().find { str == it.name } } ?: Theme.LIGHT -suspend fun PipelineContext.respondMain(body: MainTemplate.() -> Unit) { +suspend fun PipelineContext.respondMain( + noMenu: Boolean = false, + stretch: Boolean = false, + body: MainTemplate.() -> Unit +) { val param = call.request.queryParameters["theme"] if (param != null) { @@ -96,7 +113,9 @@ suspend fun PipelineContext.respondMain(body: MainTemplat } else { call.respondHtmlTemplate( MainTemplate( - call.request.cookies["theme"].toTheme() + call.request.cookies["theme"].toTheme(), + noMenu, + stretch ), body = body ) diff --git a/src/jvmMain/resources/error.html b/src/jvmMain/resources/error.html new file mode 100644 index 0000000..d110e76 --- /dev/null +++ b/src/jvmMain/resources/error.html @@ -0,0 +1,22 @@ + + + + + + KIF Portal + + + + + + +
+
+
+ Their was an error without an error... + How did you do that? +
+
+
+ + diff --git a/src/jvmMain/resources/error404.html b/src/jvmMain/resources/error404.html new file mode 100644 index 0000000..f0d2285 --- /dev/null +++ b/src/jvmMain/resources/error404.html @@ -0,0 +1,22 @@ + + + + + + KIF Portal + + + + + + +
+
+
+ Their was a 404. + It seems you lost the game. +
+
+
+ + diff --git a/src/jvmMain/resources/error500.html b/src/jvmMain/resources/error500.html new file mode 100644 index 0000000..74e4e31 --- /dev/null +++ b/src/jvmMain/resources/error500.html @@ -0,0 +1,22 @@ + + + + + + KIF Portal + + + + + + +
+
+
+ Their was a 500. + It seems we lost the game. +
+
+
+ +